From 389f6f43348a562a9c7f3cb7d5e7d05db69250d3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 1 Jul 2022 20:17:40 +0100 Subject: [PATCH] Split electron-main into smaller chunks (#377) * Split electron-main into smaller chunks * Affix @types/node version and upgrade electron-store * Iterate PR * tidy up * Actually run the split out code --- package.json | 5 +- src/@types/global.d.ts | 22 +- src/electron-main.ts | 700 ++++------------------------------------- src/ipc.ts | 202 ++++++++++++ src/keytar.ts | 31 ++ src/seshat.ts | 325 +++++++++++++++++++ src/settings.ts | 77 +++++ src/utils.ts | 29 ++ tsconfig.json | 1 + yarn.lock | 93 +++--- 10 files changed, 794 insertions(+), 691 deletions(-) create mode 100644 src/ipc.ts create mode 100644 src/keytar.ts create mode 100644 src/seshat.ts create mode 100644 src/settings.ts create mode 100644 src/utils.ts diff --git a/package.json b/package.json index 69f1dad..b148f27 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "dependencies": { "auto-launch": "^5.0.5", "counterpart": "^0.18.6", - "electron-store": "^6.0.1", + "electron-store": "^8.0.2", "electron-window-state": "^5.0.3", "minimist": "^1.2.6", "png-to-ico": "^2.1.1", @@ -88,6 +88,9 @@ "matrix-seshat": "^2.3.3", "keytar": "^7.9.0" }, + "resolutions": { + "@types/node": "16.11.38" + }, "build": { "appId": "im.riot.app", "asarUnpack": "**/*.node", diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 082e2b0..339cf0e 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -1,5 +1,5 @@ /* -Copyright 2021 New Vector Ltd +Copyright 2021 - 2022 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,12 +15,32 @@ limitations under the License. */ import { BrowserWindow } from "electron"; +import Store from "electron-store"; +import AutoLaunch from "auto-launch"; + +import { AppLocalization } from "../language-helper"; declare global { namespace NodeJS { interface Global { mainWindow: BrowserWindow; appQuitting: boolean; + appLocalization: AppLocalization; + launcher: AutoLaunch; + vectorConfig: Record; + trayConfig: { + // eslint-disable-next-line camelcase + icon_path: string; + brand: string; + }; + store: Store<{ + warnBeforeExit?: boolean; + minimizeToTray?: boolean; + spellCheckerEnabled?: boolean; + autoHideMenuBar?: boolean; + locale?: string | string[]; + disableHardwareAcceleration?: boolean; + }>; } } } diff --git a/src/electron-main.ts b/src/electron-main.ts index 8256c0f..4ee2b37 100644 --- a/src/electron-main.ts +++ b/src/electron-main.ts @@ -21,90 +21,42 @@ limitations under the License. import "./squirrelhooks"; import { app, - ipcMain, - powerSaveBlocker, BrowserWindow, Menu, autoUpdater, protocol, dialog, - desktopCapturer, } from "electron"; import AutoLaunch from "auto-launch"; import path from "path"; import windowStateKeeper from 'electron-window-state'; import Store from 'electron-store'; import fs, { promises as afs } from "fs"; -import crypto from "crypto"; import { URL } from "url"; import minimist from "minimist"; -import type * as Keytar from "keytar"; // Hak dependency type -import type { - Seshat as SeshatType, - SeshatRecovery as SeshatRecoveryType, - ReindexError as ReindexErrorType, -} from "matrix-seshat"; // Hak dependency type +import "./ipc"; +import "./keytar"; +import "./seshat"; +import "./settings"; import * as tray from "./tray"; import { buildMenuTemplate } from './vectormenu'; import webContentsHandler from './webcontents-handler'; import * as updater from './updater'; -import { getProfileFromDeeplink, protocolInit, recordSSOSession } from './protocol'; +import { getProfileFromDeeplink, protocolInit } from './protocol'; import { _t, AppLocalization } from './language-helper'; import Input = Electron.Input; -import IpcMainEvent = Electron.IpcMainEvent; const argv = minimist(process.argv, { alias: { help: "h" }, }); -let keytar: typeof Keytar; -try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - keytar = require('keytar'); -} catch (e) { - if (e.code === "MODULE_NOT_FOUND") { - console.log("Keytar isn't installed; secure key storage is disabled."); - } else { - console.warn("Keytar unexpected error:", e); - } -} - -let seshatSupported = false; -let Seshat: typeof SeshatType; -let SeshatRecovery: typeof SeshatRecoveryType; -let ReindexError: typeof ReindexErrorType; - -try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const seshatModule = require('matrix-seshat'); - Seshat = seshatModule.Seshat; - SeshatRecovery = seshatModule.SeshatRecovery; - ReindexError = seshatModule.ReindexError; - seshatSupported = true; -} catch (e) { - if (e.code === "MODULE_NOT_FOUND") { - console.log("Seshat isn't installed, event indexing is disabled."); - } else { - console.warn("Seshat unexpected error:", e); - } -} - // Things we need throughout the file but need to be created // async to are initialised in setupGlobals() let asarPath: string; let resPath: string; let iconPath: string; -let vectorConfig: Record; -let trayConfig: { - // eslint-disable-next-line camelcase - icon_path: string; - brand: string; -}; -let launcher: AutoLaunch; -let appLocalization: AppLocalization; - if (argv["help"]) { console.log("Options:"); console.log(" --profile-dir {path}: Path to where to store the profile."); @@ -199,13 +151,13 @@ async function setupGlobals(): Promise { try { // eslint-disable-next-line @typescript-eslint/no-var-requires - vectorConfig = require(asarPath + 'config.json'); + global.vectorConfig = require(asarPath + 'config.json'); } catch (e) { // it would be nice to check the error code here and bail if the config // is unparsable, but we get MODULE_NOT_FOUND in the case of a missing // file or invalid json, so node is just very unhelpful. // Continue with the defaults (ie. an empty config) - vectorConfig = {}; + global.vectorConfig = {}; } try { @@ -219,19 +171,19 @@ async function setupGlobals(): Promise { const homeserverProps = ['default_is_url', 'default_hs_url', 'default_server_name', 'default_server_config']; if (Object.keys(localConfig).find(k => homeserverProps.includes(k))) { // Rip out all the homeserver options from the vector config - vectorConfig = Object.keys(vectorConfig) + global.vectorConfig = Object.keys(global.vectorConfig) .filter(k => !homeserverProps.includes(k)) - .reduce((obj, key) => {obj[key] = vectorConfig[key]; return obj;}, {}); + .reduce((obj, key) => {obj[key] = global.vectorConfig[key]; return obj;}, {}); } - vectorConfig = Object.assign(vectorConfig, localConfig); + global.vectorConfig = Object.assign(global.vectorConfig, localConfig); } catch (e) { if (e instanceof SyntaxError) { dialog.showMessageBox({ type: "error", - title: `Your ${vectorConfig.brand || 'Element'} is misconfigured`, - message: `Your custom ${vectorConfig.brand || 'Element'} configuration contains invalid JSON. ` + - `Please correct the problem and reopen ${vectorConfig.brand || 'Element'}.`, + title: `Your ${global.vectorConfig.brand || 'Element'} is misconfigured`, + message: `Your custom ${global.vectorConfig.brand || 'Element'} configuration contains invalid JSON. ` + + `Please correct the problem and reopen ${global.vectorConfig.brand || 'Element'}.`, detail: e.message || "", }); } @@ -243,14 +195,14 @@ async function setupGlobals(): Promise { // It's important to call `path.join` so we don't end up with the packaged asar in the final path. const iconFile = `element.${process.platform === 'win32' ? 'ico' : 'png'}`; iconPath = path.join(resPath, "img", iconFile); - trayConfig = { + global.trayConfig = { icon_path: iconPath, - brand: vectorConfig.brand || 'Element', + brand: global.vectorConfig.brand || 'Element', }; // launcher - launcher = new AutoLaunch({ - name: vectorConfig.brand || 'Element', + global.launcher = new AutoLaunch({ + name: global.vectorConfig.brand || 'Element', isHidden: true, mac: { useLaunchAgent: true, @@ -261,7 +213,7 @@ async function setupGlobals(): Promise { async function moveAutoLauncher(): Promise { // Look for an auto-launcher under 'Riot' and if we find one, port it's // enabled/disabled-ness over to the new 'Element' launcher - if (!vectorConfig.brand || vectorConfig.brand === 'Element') { + if (!global.vectorConfig.brand || global.vectorConfig.brand === 'Element') { const oldLauncher = new AutoLaunch({ name: 'Riot', isHidden: true, @@ -272,24 +224,13 @@ async function moveAutoLauncher(): Promise { const wasEnabled = await oldLauncher.isEnabled(); if (wasEnabled) { await oldLauncher.disable(); - await launcher.enable(); + await global.launcher.enable(); } } } -const eventStorePath = path.join(app.getPath('userData'), 'EventStore'); -const store = new Store<{ - warnBeforeExit?: boolean; - minimizeToTray?: boolean; - spellCheckerEnabled?: boolean; - autoHideMenuBar?: boolean; - locale?: string | string[]; - disableHardwareAcceleration?: boolean; -}>({ name: "electron-config" }); +global.store = new Store({ name: "electron-config" }); -let eventIndex: SeshatType = null; - -let mainWindow: BrowserWindow = null; global.appQuitting = false; const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [ @@ -299,12 +240,12 @@ const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [ ]; const warnBeforeExit = (event: Event, input: Input): void => { - const shouldWarnBeforeExit = store.get('warnBeforeExit', true); + const shouldWarnBeforeExit = global.store.get('warnBeforeExit', true); const exitShortcutPressed = input.type === 'keyDown' && exitShortcuts.some(shortcutFn => shortcutFn(input, process.platform)); if (shouldWarnBeforeExit && exitShortcutPressed) { - const shouldCancelCloseRequest = dialog.showMessageBoxSync(mainWindow, { + const shouldCancelCloseRequest = dialog.showMessageBoxSync(global.mainWindow, { type: "question", buttons: [_t("Cancel"), _t("Close Element")], message: _t("Are you sure you want to quit?"), @@ -318,25 +259,6 @@ const warnBeforeExit = (event: Event, input: Input): void => { } }; -const deleteContents = async (p: string): Promise => { - for (const entry of await afs.readdir(p)) { - const curPath = path.join(p, entry); - await afs.unlink(curPath); - } -}; - -async function randomArray(size: number): Promise { - return new Promise((resolve, reject) => { - crypto.randomBytes(size, (err, buf) => { - if (err) { - reject(err); - } else { - resolve(buf.toString("base64").replace(/=+$/g, '')); - } - }); - }); -} - // handle uncaught errors otherwise it displays // stack traces in popup dialogs, which is terrible (which // it will do any time the auto update poke fails, and there's @@ -347,510 +269,6 @@ process.on('uncaughtException', function(error: Error): void { console.log('Unhandled exception', error); }); -let focusHandlerAttached = false; -ipcMain.on('setBadgeCount', function(_ev: IpcMainEvent, count: number): void { - if (process.platform !== 'win32') { - // only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron - // has some Windows support too, and in some Windows environments this leads to two badges rendering atop - // each other. See https://github.com/vector-im/element-web/issues/16942 - app.badgeCount = count; - } - if (count === 0 && mainWindow) { - mainWindow.flashFrame(false); - } -}); - -ipcMain.on('loudNotification', function(): void { - if (process.platform === 'win32' && mainWindow && !mainWindow.isFocused() && !focusHandlerAttached) { - mainWindow.flashFrame(true); - mainWindow.once('focus', () => { - mainWindow.flashFrame(false); - focusHandlerAttached = false; - }); - focusHandlerAttached = true; - } -}); - -let powerSaveBlockerId: number = null; -ipcMain.on('app_onAction', function(_ev: IpcMainEvent, payload) { - switch (payload.action) { - case 'call_state': - if (powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId)) { - if (payload.state === 'ended') { - powerSaveBlocker.stop(powerSaveBlockerId); - powerSaveBlockerId = null; - } - } else { - if (powerSaveBlockerId === null && payload.state === 'connected') { - powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep'); - } - } - break; - } -}); - -interface Setting { - read(): Promise; - write(value: any): Promise; -} - -const settings: Record = { - "Electron.autoLaunch": { - async read(): Promise { - return launcher.isEnabled(); - }, - async write(value: any): Promise { - if (value) { - return launcher.enable(); - } else { - return launcher.disable(); - } - }, - }, - "Electron.warnBeforeExit": { - async read(): Promise { - return store.get("warnBeforeExit", true); - }, - async write(value: any): Promise { - store.set("warnBeforeExit", value); - }, - }, - "Electron.alwaysShowMenuBar": { // not supported on macOS - async read(): Promise { - return !global.mainWindow.autoHideMenuBar; - }, - async write(value: any): Promise { - store.set('autoHideMenuBar', !value); - global.mainWindow.autoHideMenuBar = !value; - global.mainWindow.setMenuBarVisibility(value); - }, - }, - "Electron.showTrayIcon": { // not supported on macOS - async read(): Promise { - return tray.hasTray(); - }, - async write(value: any): Promise { - if (value) { - // Create trayIcon icon - tray.create(trayConfig); - } else { - tray.destroy(); - } - store.set('minimizeToTray', value); - }, - }, - "Electron.enableHardwareAcceleration": { - async read(): Promise { - return !store.get('disableHardwareAcceleration', false); - }, - async write(value: any): Promise { - store.set('disableHardwareAcceleration', !value); - }, - }, -}; - -ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) { - if (!mainWindow) return; - - const args = payload.args || []; - let ret: any; - - switch (payload.name) { - case 'getUpdateFeedUrl': - ret = autoUpdater.getFeedURL(); - break; - case 'getSettingValue': { - const [settingName] = args; - const setting = settings[settingName]; - ret = await setting.read(); - break; - } - case 'setSettingValue': { - const [settingName, value] = args; - const setting = settings[settingName]; - await setting.write(value); - break; - } - case 'setLanguage': - appLocalization.setAppLocale(args[0]); - break; - case 'getAppVersion': - ret = app.getVersion(); - break; - case 'focusWindow': - if (mainWindow.isMinimized()) { - mainWindow.restore(); - } else if (!mainWindow.isVisible()) { - mainWindow.show(); - } else { - mainWindow.focus(); - } - break; - case 'getConfig': - ret = vectorConfig; - break; - case 'navigateBack': - if (mainWindow.webContents.canGoBack()) { - mainWindow.webContents.goBack(); - } - break; - case 'navigateForward': - if (mainWindow.webContents.canGoForward()) { - mainWindow.webContents.goForward(); - } - break; - case 'setSpellCheckLanguages': - if (args[0] && args[0].length > 0) { - mainWindow.webContents.session.setSpellCheckerEnabled(true); - store.set("spellCheckerEnabled", true); - - try { - mainWindow.webContents.session.setSpellCheckerLanguages(args[0]); - } catch (er) { - console.log("There were problems setting the spellcheck languages", er); - } - } else { - mainWindow.webContents.session.setSpellCheckerEnabled(false); - store.set("spellCheckerEnabled", false); - } - break; - case 'getSpellCheckLanguages': - if (store.get("spellCheckerEnabled", true)) { - ret = mainWindow.webContents.session.getSpellCheckerLanguages(); - } else { - ret = []; - } - break; - case 'getAvailableSpellCheckLanguages': - ret = mainWindow.webContents.session.availableSpellCheckerLanguages; - break; - - case 'startSSOFlow': - recordSSOSession(args[0]); - break; - - case 'getPickleKey': - try { - ret = await keytar.getPassword("element.io", `${args[0]}|${args[1]}`); - // migrate from riot.im (remove once we think there will no longer be - // logins from the time of riot.im) - if (ret === null) { - ret = await keytar.getPassword("riot.im", `${args[0]}|${args[1]}`); - } - } catch (e) { - // if an error is thrown (e.g. keytar can't connect to the keychain), - // then return null, which means the default pickle key will be used - ret = null; - } - break; - - case 'createPickleKey': - try { - const pickleKey = await randomArray(32); - await keytar.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey); - ret = pickleKey; - } catch (e) { - ret = null; - } - break; - - case 'destroyPickleKey': - try { - await keytar.deletePassword("element.io", `${args[0]}|${args[1]}`); - // migrate from riot.im (remove once we think there will no longer be - // logins from the time of riot.im) - await keytar.deletePassword("riot.im", `${args[0]}|${args[1]}`); - } catch (e) {} - break; - case 'getDesktopCapturerSources': - ret = (await desktopCapturer.getSources(args[0])).map((source) => ({ - id: source.id, - name: source.name, - thumbnailURL: source.thumbnail.toDataURL(), - })); - break; - - default: - mainWindow.webContents.send('ipcReply', { - id: payload.id, - error: "Unknown IPC Call: " + payload.name, - }); - return; - } - - mainWindow.webContents.send('ipcReply', { - id: payload.id, - reply: ret, - }); -}); - -const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE"; -async function getOrCreatePassphrase(key: string): Promise { - if (keytar) { - try { - const storedPassphrase = await keytar.getPassword("element.io", key); - if (storedPassphrase !== null) { - return storedPassphrase; - } else { - const newPassphrase = await randomArray(32); - await keytar.setPassword("element.io", key, newPassphrase); - return newPassphrase; - } - } catch (e) { - console.log("Error getting the event index passphrase out of the secret store", e); - } - } else { - return seshatDefaultPassphrase; - } -} - -ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise { - if (!mainWindow) return; - - const sendError = (id, e) => { - const error = { - message: e.message, - }; - - mainWindow.webContents.send('seshatReply', { - id: id, - error: error, - }); - }; - - const args = payload.args || []; - let ret: any; - - switch (payload.name) { - case 'supportsEventIndexing': - ret = seshatSupported; - break; - - case 'initEventIndex': - if (eventIndex === null) { - const userId = args[0]; - const deviceId = args[1]; - const passphraseKey = `seshat|${userId}|${deviceId}`; - - const passphrase = await getOrCreatePassphrase(passphraseKey); - - try { - await afs.mkdir(eventStorePath, { recursive: true }); - eventIndex = new Seshat(eventStorePath, { passphrase }); - } catch (e) { - if (e instanceof ReindexError) { - // If this is a reindex error, the index schema - // changed. Try to open the database in recovery mode, - // reindex the database and finally try to open the - // database again. - const recoveryIndex = new SeshatRecovery(eventStorePath, { - passphrase, - }); - - const userVersion = await recoveryIndex.getUserVersion(); - - // If our user version is 0 we'll delete the db - // anyways so reindexing it is a waste of time. - if (userVersion === 0) { - await recoveryIndex.shutdown(); - - try { - await deleteContents(eventStorePath); - } catch (e) { - } - } else { - await recoveryIndex.reindex(); - } - - eventIndex = new Seshat(eventStorePath, { passphrase }); - } else { - sendError(payload.id, e); - return; - } - } - } - break; - - case 'closeEventIndex': - if (eventIndex !== null) { - const index = eventIndex; - eventIndex = null; - - try { - await index.shutdown(); - } catch (e) { - sendError(payload.id, e); - return; - } - } - break; - - case 'deleteEventIndex': - { - try { - await deleteContents(eventStorePath); - } catch (e) { - } - } - - break; - - case 'isEventIndexEmpty': - if (eventIndex === null) ret = true; - else ret = await eventIndex.isEmpty(); - break; - - case 'isRoomIndexed': - if (eventIndex === null) ret = false; - else ret = await eventIndex.isRoomIndexed(args[0]); - break; - - case 'addEventToIndex': - try { - eventIndex.addEvent(args[0], args[1]); - } catch (e) { - sendError(payload.id, e); - return; - } - break; - - case 'deleteEvent': - try { - ret = await eventIndex.deleteEvent(args[0]); - } catch (e) { - sendError(payload.id, e); - return; - } - break; - - case 'commitLiveEvents': - try { - ret = await eventIndex.commit(); - } catch (e) { - sendError(payload.id, e); - return; - } - break; - - case 'searchEventIndex': - try { - ret = await eventIndex.search(args[0]); - } catch (e) { - sendError(payload.id, e); - return; - } - break; - - case 'addHistoricEvents': - if (eventIndex === null) ret = false; - else { - try { - ret = await eventIndex.addHistoricEvents( - args[0], args[1], args[2]); - } catch (e) { - sendError(payload.id, e); - return; - } - } - break; - - case 'getStats': - if (eventIndex === null) ret = 0; - else { - try { - ret = await eventIndex.getStats(); - } catch (e) { - sendError(payload.id, e); - return; - } - } - break; - - case 'removeCrawlerCheckpoint': - if (eventIndex === null) ret = false; - else { - try { - ret = await eventIndex.removeCrawlerCheckpoint(args[0]); - } catch (e) { - sendError(payload.id, e); - return; - } - } - break; - - case 'addCrawlerCheckpoint': - if (eventIndex === null) ret = false; - else { - try { - ret = await eventIndex.addCrawlerCheckpoint(args[0]); - } catch (e) { - sendError(payload.id, e); - return; - } - } - break; - - case 'loadFileEvents': - if (eventIndex === null) ret = []; - else { - try { - ret = await eventIndex.loadFileEvents(args[0]); - } catch (e) { - sendError(payload.id, e); - return; - } - } - break; - - case 'loadCheckpoints': - if (eventIndex === null) ret = []; - else { - try { - ret = await eventIndex.loadCheckpoints(); - } catch (e) { - ret = []; - } - } - break; - - case 'setUserVersion': - if (eventIndex === null) break; - else { - try { - await eventIndex.setUserVersion(args[0]); - } catch (e) { - sendError(payload.id, e); - return; - } - } - break; - - case 'getUserVersion': - if (eventIndex === null) ret = 0; - else { - try { - ret = await eventIndex.getUserVersion(); - } catch (e) { - sendError(payload.id, e); - return; - } - } - break; - - default: - mainWindow.webContents.send('seshatReply', { - id: payload.id, - error: "Unknown IPC Call: " + payload.name, - }); - return; - } - - mainWindow.webContents.send('seshatReply', { - id: payload.id, - reply: ret, - }); -}); - app.commandLine.appendSwitch('--enable-usermedia-screen-capturing'); if (!app.commandLine.hasSwitch('enable-features')) { app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer'); @@ -894,7 +312,7 @@ app.enableSandbox(); app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService'); // Disable hardware acceleration if the setting has been set. -if (store.get('disableHardwareAcceleration', false) === true) { +if (global.store.get('disableHardwareAcceleration', false) === true) { console.log("Disabling hardware acceleration."); app.disableHardwareAcceleration(); } @@ -982,9 +400,9 @@ app.on('ready', async () => { if (argv['no-update']) { console.log('Auto update disabled via command line flag "--no-update"'); - } else if (vectorConfig['update_base_url']) { - console.log(`Starting auto update with base URL: ${vectorConfig['update_base_url']}`); - updater.start(vectorConfig['update_base_url']); + } else if (global.vectorConfig['update_base_url']) { + console.log(`Starting auto update with base URL: ${global.vectorConfig['update_base_url']}`); + updater.start(global.vectorConfig['update_base_url']); } else { console.log('No update_base_url is defined: auto update is disabled'); } @@ -996,13 +414,13 @@ app.on('ready', async () => { }); const preloadScript = path.normalize(`${__dirname}/preload.js`); - mainWindow = global.mainWindow = new BrowserWindow({ + global.mainWindow = new BrowserWindow({ // https://www.electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do backgroundColor: '#fff', icon: iconPath, show: false, - autoHideMenuBar: store.get('autoHideMenuBar', true), + autoHideMenuBar: global.store.get('autoHideMenuBar', true), x: mainWindowState.x, y: mainWindowState.y, @@ -1016,32 +434,32 @@ app.on('ready', async () => { webgl: true, }, }); - mainWindow.loadURL('vector://vector/webapp/'); + global.mainWindow.loadURL('vector://vector/webapp/'); // Handle spellchecker - // For some reason spellCheckerEnabled isn't persisted so we have to use the store here - mainWindow.webContents.session.setSpellCheckerEnabled(store.get("spellCheckerEnabled", true)); + // For some reason spellCheckerEnabled isn't persisted, so we have to use the store here + global.mainWindow.webContents.session.setSpellCheckerEnabled(global.store.get("spellCheckerEnabled", true)); // Create trayIcon icon - if (store.get('minimizeToTray', true)) tray.create(trayConfig); + if (global.store.get('minimizeToTray', true)) tray.create(global.trayConfig); - mainWindow.once('ready-to-show', () => { - mainWindowState.manage(mainWindow); + global.mainWindow.once('ready-to-show', () => { + mainWindowState.manage(global.mainWindow); if (!argv['hidden']) { - mainWindow.show(); + global.mainWindow.show(); } else { // hide here explicitly because window manage above sometimes shows it - mainWindow.hide(); + global.mainWindow.hide(); } }); - mainWindow.webContents.on('before-input-event', warnBeforeExit); + global.mainWindow.webContents.on('before-input-event', warnBeforeExit); - mainWindow.on('closed', () => { - mainWindow = global.mainWindow = null; + global.mainWindow.on('closed', () => { + global.mainWindow = null; }); - mainWindow.on('close', async (e) => { + global.mainWindow.on('close', async (e) => { // If we are not quitting and have a tray icon then minimize to tray if (!global.appQuitting && (tray.hasTray() || process.platform === 'darwin')) { // On Mac, closing the window just hides it @@ -1049,12 +467,12 @@ app.on('ready', async () => { // behave, eg. Mail.app) e.preventDefault(); - if (mainWindow.isFullScreen()) { - mainWindow.once('leave-full-screen', () => mainWindow.hide()); + if (global.mainWindow.isFullScreen()) { + global.mainWindow.once('leave-full-screen', () => global.mainWindow.hide()); - mainWindow.setFullScreen(false); + global.mainWindow.setFullScreen(false); } else { - mainWindow.hide(); + global.mainWindow.hide(); } return false; @@ -1063,19 +481,19 @@ app.on('ready', async () => { if (process.platform === 'win32') { // Handle forward/backward mouse buttons in Windows - mainWindow.on('app-command', (e, cmd) => { - if (cmd === 'browser-backward' && mainWindow.webContents.canGoBack()) { - mainWindow.webContents.goBack(); - } else if (cmd === 'browser-forward' && mainWindow.webContents.canGoForward()) { - mainWindow.webContents.goForward(); + global.mainWindow.on('app-command', (e, cmd) => { + if (cmd === 'browser-backward' && global.mainWindow.webContents.canGoBack()) { + global.mainWindow.webContents.goBack(); + } else if (cmd === 'browser-forward' && global.mainWindow.webContents.canGoForward()) { + global.mainWindow.webContents.goForward(); } }); } - webContentsHandler(mainWindow.webContents); + webContentsHandler(global.mainWindow.webContents); - appLocalization = new AppLocalization({ - store, + global.appLocalization = new AppLocalization({ + store: global.store, components: [ () => tray.initApplicationMenu(), () => Menu.setApplicationMenu(buildMenuTemplate()), @@ -1088,14 +506,12 @@ app.on('window-all-closed', () => { }); app.on('activate', () => { - mainWindow.show(); + global.mainWindow.show(); }); function beforeQuit(): void { global.appQuitting = true; - if (mainWindow) { - mainWindow.webContents.send('before-quit'); - } + global.mainWindow?.webContents.send('before-quit'); } app.on('before-quit', beforeQuit); @@ -1106,10 +522,10 @@ app.on('second-instance', (ev, commandLine, workingDirectory) => { if (commandLine.includes('--hidden')) return; // Someone tried to run a second instance, we should focus our window. - if (mainWindow) { - if (!mainWindow.isVisible()) mainWindow.show(); - if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.focus(); + if (global.mainWindow) { + if (!global.mainWindow.isVisible()) global.mainWindow.show(); + if (global.mainWindow.isMinimized()) global.mainWindow.restore(); + global.mainWindow.focus(); } }); diff --git a/src/ipc.ts b/src/ipc.ts new file mode 100644 index 0000000..8b39376 --- /dev/null +++ b/src/ipc.ts @@ -0,0 +1,202 @@ +/* +Copyright 2022 New Vector Ltd + +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 { app, autoUpdater, desktopCapturer, ipcMain, powerSaveBlocker } from "electron"; + +import IpcMainEvent = Electron.IpcMainEvent; +import { recordSSOSession } from "./protocol"; +import { randomArray } from "./utils"; +import { Settings } from "./settings"; +import { keytar } from "./keytar"; + +ipcMain.on('setBadgeCount', function(_ev: IpcMainEvent, count: number): void { + if (process.platform !== 'win32') { + // only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron + // has some Windows support too, and in some Windows environments this leads to two badges rendering atop + // each other. See https://github.com/vector-im/element-web/issues/16942 + app.badgeCount = count; + } + if (count === 0) { + global.mainWindow?.flashFrame(false); + } +}); + +let focusHandlerAttached = false; +ipcMain.on('loudNotification', function(): void { + if (process.platform === 'win32' && global.mainWindow && !global.mainWindow.isFocused() && !focusHandlerAttached) { + global.mainWindow.flashFrame(true); + global.mainWindow.once('focus', () => { + global.mainWindow.flashFrame(false); + focusHandlerAttached = false; + }); + focusHandlerAttached = true; + } +}); + +let powerSaveBlockerId: number = null; +ipcMain.on('app_onAction', function(_ev: IpcMainEvent, payload) { + switch (payload.action) { + case 'call_state': { + if (powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId)) { + if (payload.state === 'ended') { + powerSaveBlocker.stop(powerSaveBlockerId); + powerSaveBlockerId = null; + } + } else { + if (powerSaveBlockerId === null && payload.state === 'connected') { + powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep'); + } + } + break; + } + } +}); + +ipcMain.on('ipcCall', async function(_ev: IpcMainEvent, payload) { + if (!global.mainWindow) return; + + const args = payload.args || []; + let ret: any; + + switch (payload.name) { + case 'getUpdateFeedUrl': + ret = autoUpdater.getFeedURL(); + break; + case 'getSettingValue': { + const [settingName] = args; + const setting = Settings[settingName]; + ret = await setting.read(); + break; + } + case 'setSettingValue': { + const [settingName, value] = args; + const setting = Settings[settingName]; + await setting.write(value); + break; + } + case 'setLanguage': + global.appLocalization.setAppLocale(args[0]); + break; + case 'getAppVersion': + ret = app.getVersion(); + break; + case 'focusWindow': + if (global.mainWindow.isMinimized()) { + global.mainWindow.restore(); + } else if (!global.mainWindow.isVisible()) { + global.mainWindow.show(); + } else { + global.mainWindow.focus(); + } + break; + case 'getConfig': + ret = global.vectorConfig; + break; + case 'navigateBack': + if (global.mainWindow.webContents.canGoBack()) { + global.mainWindow.webContents.goBack(); + } + break; + case 'navigateForward': + if (global.mainWindow.webContents.canGoForward()) { + global.mainWindow.webContents.goForward(); + } + break; + case 'setSpellCheckLanguages': + if (args[0] && args[0].length > 0) { + global.mainWindow.webContents.session.setSpellCheckerEnabled(true); + global.store.set("spellCheckerEnabled", true); + + try { + global.mainWindow.webContents.session.setSpellCheckerLanguages(args[0]); + } catch (er) { + console.log("There were problems setting the spellcheck languages", er); + } + } else { + global.mainWindow.webContents.session.setSpellCheckerEnabled(false); + global.store.set("spellCheckerEnabled", false); + } + break; + case 'getSpellCheckLanguages': + if (global.store.get("spellCheckerEnabled", true)) { + ret = global.mainWindow.webContents.session.getSpellCheckerLanguages(); + } else { + ret = []; + } + break; + case 'getAvailableSpellCheckLanguages': + ret = global.mainWindow.webContents.session.availableSpellCheckerLanguages; + break; + + case 'startSSOFlow': + recordSSOSession(args[0]); + break; + + case 'getPickleKey': + try { + ret = await keytar.getPassword("element.io", `${args[0]}|${args[1]}`); + // migrate from riot.im (remove once we think there will no longer be + // logins from the time of riot.im) + if (ret === null) { + ret = await keytar.getPassword("riot.im", `${args[0]}|${args[1]}`); + } + } catch (e) { + // if an error is thrown (e.g. keytar can't connect to the keychain), + // then return null, which means the default pickle key will be used + ret = null; + } + break; + + case 'createPickleKey': + try { + const pickleKey = await randomArray(32); + await keytar.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey); + ret = pickleKey; + } catch (e) { + ret = null; + } + break; + + case 'destroyPickleKey': + try { + await keytar.deletePassword("element.io", `${args[0]}|${args[1]}`); + // migrate from riot.im (remove once we think there will no longer be + // logins from the time of riot.im) + await keytar.deletePassword("riot.im", `${args[0]}|${args[1]}`); + } catch (e) {} + break; + case 'getDesktopCapturerSources': + ret = (await desktopCapturer.getSources(args[0])).map((source) => ({ + id: source.id, + name: source.name, + thumbnailURL: source.thumbnail.toDataURL(), + })); + break; + + default: + global.mainWindow.webContents.send('ipcReply', { + id: payload.id, + error: "Unknown IPC Call: " + payload.name, + }); + return; + } + + global.mainWindow.webContents.send('ipcReply', { + id: payload.id, + reply: ret, + }); +}); + diff --git a/src/keytar.ts b/src/keytar.ts new file mode 100644 index 0000000..58d3436 --- /dev/null +++ b/src/keytar.ts @@ -0,0 +1,31 @@ +/* +Copyright 2022 New Vector Ltd + +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 type * as Keytar from "keytar"; // Hak dependency type + +let keytar: typeof Keytar | undefined; +try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + keytar = require('keytar'); +} catch (e) { + if (e.code === "MODULE_NOT_FOUND") { + console.log("Keytar isn't installed; secure key storage is disabled."); + } else { + console.warn("Keytar unexpected error:", e); + } +} + +export { keytar }; diff --git a/src/seshat.ts b/src/seshat.ts new file mode 100644 index 0000000..49d48d3 --- /dev/null +++ b/src/seshat.ts @@ -0,0 +1,325 @@ +/* +Copyright 2022 New Vector Ltd + +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 { app, ipcMain } from "electron"; +import { promises as afs } from "fs"; +import path from "path"; + +import type { + Seshat as SeshatType, + SeshatRecovery as SeshatRecoveryType, + ReindexError as ReindexErrorType, +} from "matrix-seshat"; // Hak dependency type +import IpcMainEvent = Electron.IpcMainEvent; +import { randomArray } from "./utils"; +import { keytar } from "./keytar"; + +let seshatSupported = false; +let Seshat: typeof SeshatType; +let SeshatRecovery: typeof SeshatRecoveryType; +let ReindexError: typeof ReindexErrorType; + +try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const seshatModule = require('matrix-seshat'); + Seshat = seshatModule.Seshat; + SeshatRecovery = seshatModule.SeshatRecovery; + ReindexError = seshatModule.ReindexError; + seshatSupported = true; +} catch (e) { + if (e.code === "MODULE_NOT_FOUND") { + console.log("Seshat isn't installed, event indexing is disabled."); + } else { + console.warn("Seshat unexpected error:", e); + } +} + +const eventStorePath = path.join(app.getPath('userData'), 'EventStore'); + +let eventIndex: SeshatType = null; + +const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE"; +async function getOrCreatePassphrase(key: string): Promise { + if (keytar) { + try { + const storedPassphrase = await keytar.getPassword("element.io", key); + if (storedPassphrase !== null) { + return storedPassphrase; + } else { + const newPassphrase = await randomArray(32); + await keytar.setPassword("element.io", key, newPassphrase); + return newPassphrase; + } + } catch (e) { + console.log("Error getting the event index passphrase out of the secret store", e); + } + } else { + return seshatDefaultPassphrase; + } +} + +const deleteContents = async (p: string): Promise => { + for (const entry of await afs.readdir(p)) { + const curPath = path.join(p, entry); + await afs.unlink(curPath); + } +}; + +ipcMain.on('seshat', async function(_ev: IpcMainEvent, payload): Promise { + if (!global.mainWindow) return; + + const sendError = (id, e) => { + const error = { + message: e.message, + }; + + global.mainWindow.webContents.send('seshatReply', { + id: id, + error: error, + }); + }; + + const args = payload.args || []; + let ret: any; + + switch (payload.name) { + case 'supportsEventIndexing': + ret = seshatSupported; + break; + + case 'initEventIndex': + if (eventIndex === null) { + const userId = args[0]; + const deviceId = args[1]; + const passphraseKey = `seshat|${userId}|${deviceId}`; + + const passphrase = await getOrCreatePassphrase(passphraseKey); + + try { + await afs.mkdir(eventStorePath, { recursive: true }); + eventIndex = new Seshat(eventStorePath, { passphrase }); + } catch (e) { + if (e instanceof ReindexError) { + // If this is a reindex error, the index schema + // changed. Try to open the database in recovery mode, + // reindex the database and finally try to open the + // database again. + const recoveryIndex = new SeshatRecovery(eventStorePath, { + passphrase, + }); + + const userVersion = await recoveryIndex.getUserVersion(); + + // If our user version is 0 we'll delete the db + // anyways so reindexing it is a waste of time. + if (userVersion === 0) { + await recoveryIndex.shutdown(); + + try { + await deleteContents(eventStorePath); + } catch (e) { + } + } else { + await recoveryIndex.reindex(); + } + + eventIndex = new Seshat(eventStorePath, { passphrase }); + } else { + sendError(payload.id, e); + return; + } + } + } + break; + + case 'closeEventIndex': + if (eventIndex !== null) { + const index = eventIndex; + eventIndex = null; + + try { + await index.shutdown(); + } catch (e) { + sendError(payload.id, e); + return; + } + } + break; + + case 'deleteEventIndex': { + try { + await deleteContents(eventStorePath); + } catch (e) { + + } + break; + } + + case 'isEventIndexEmpty': + if (eventIndex === null) ret = true; + else ret = await eventIndex.isEmpty(); + break; + + case 'isRoomIndexed': + if (eventIndex === null) ret = false; + else ret = await eventIndex.isRoomIndexed(args[0]); + break; + + case 'addEventToIndex': + try { + eventIndex.addEvent(args[0], args[1]); + } catch (e) { + sendError(payload.id, e); + return; + } + break; + + case 'deleteEvent': + try { + ret = await eventIndex.deleteEvent(args[0]); + } catch (e) { + sendError(payload.id, e); + return; + } + break; + + case 'commitLiveEvents': + try { + ret = await eventIndex.commit(); + } catch (e) { + sendError(payload.id, e); + return; + } + break; + + case 'searchEventIndex': + try { + ret = await eventIndex.search(args[0]); + } catch (e) { + sendError(payload.id, e); + return; + } + break; + + case 'addHistoricEvents': + if (eventIndex === null) ret = false; + else { + try { + ret = await eventIndex.addHistoricEvents( + args[0], args[1], args[2]); + } catch (e) { + sendError(payload.id, e); + return; + } + } + break; + + case 'getStats': + if (eventIndex === null) ret = 0; + else { + try { + ret = await eventIndex.getStats(); + } catch (e) { + sendError(payload.id, e); + return; + } + } + break; + + case 'removeCrawlerCheckpoint': + if (eventIndex === null) ret = false; + else { + try { + ret = await eventIndex.removeCrawlerCheckpoint(args[0]); + } catch (e) { + sendError(payload.id, e); + return; + } + } + break; + + case 'addCrawlerCheckpoint': + if (eventIndex === null) ret = false; + else { + try { + ret = await eventIndex.addCrawlerCheckpoint(args[0]); + } catch (e) { + sendError(payload.id, e); + return; + } + } + break; + + case 'loadFileEvents': + if (eventIndex === null) ret = []; + else { + try { + ret = await eventIndex.loadFileEvents(args[0]); + } catch (e) { + sendError(payload.id, e); + return; + } + } + break; + + case 'loadCheckpoints': + if (eventIndex === null) ret = []; + else { + try { + ret = await eventIndex.loadCheckpoints(); + } catch (e) { + ret = []; + } + } + break; + + case 'setUserVersion': + if (eventIndex === null) break; + else { + try { + await eventIndex.setUserVersion(args[0]); + } catch (e) { + sendError(payload.id, e); + return; + } + } + break; + + case 'getUserVersion': + if (eventIndex === null) ret = 0; + else { + try { + ret = await eventIndex.getUserVersion(); + } catch (e) { + sendError(payload.id, e); + return; + } + } + break; + + default: + global.mainWindow.webContents.send('seshatReply', { + id: payload.id, + error: "Unknown IPC Call: " + payload.name, + }); + return; + } + + global.mainWindow.webContents.send('seshatReply', { + id: payload.id, + reply: ret, + }); +}); diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..497f03f --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,77 @@ +/* +Copyright 2022 New Vector Ltd + +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 * as tray from "./tray"; + +interface Setting { + read(): Promise; + write(value: any): Promise; +} + +export const Settings: Record = { + "Electron.autoLaunch": { + async read(): Promise { + return global.launcher.isEnabled(); + }, + async write(value: any): Promise { + if (value) { + return global.launcher.enable(); + } else { + return global.launcher.disable(); + } + }, + }, + "Electron.warnBeforeExit": { + async read(): Promise { + return global.store.get("warnBeforeExit", true); + }, + async write(value: any): Promise { + global.store.set("warnBeforeExit", value); + }, + }, + "Electron.alwaysShowMenuBar": { // not supported on macOS + async read(): Promise { + return !global.mainWindow.autoHideMenuBar; + }, + async write(value: any): Promise { + global.store.set('autoHideMenuBar', !value); + global.mainWindow.autoHideMenuBar = !value; + global.mainWindow.setMenuBarVisibility(value); + }, + }, + "Electron.showTrayIcon": { // not supported on macOS + async read(): Promise { + return tray.hasTray(); + }, + async write(value: any): Promise { + if (value) { + // Create trayIcon icon + tray.create(global.trayConfig); + } else { + tray.destroy(); + } + global.store.set('minimizeToTray', value); + }, + }, + "Electron.enableHardwareAcceleration": { + async read(): Promise { + return !global.store.get('disableHardwareAcceleration', false); + }, + async write(value: any): Promise { + global.store.set('disableHardwareAcceleration', !value); + }, + }, +}; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..07b8e81 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,29 @@ +/* +Copyright 2022 New Vector Ltd + +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 crypto from "crypto"; + +export async function randomArray(size: number): Promise { + return new Promise((resolve, reject) => { + crypto.randomBytes(size, (err, buf) => { + if (err) { + reject(err); + } else { + resolve(buf.toString("base64").replace(/=+$/g, '')); + } + }); + }); +} diff --git a/tsconfig.json b/tsconfig.json index 0d8142a..4b14e79 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "outDir": "./lib", "rootDir": "./src", "declaration": true, + "typeRoots": ["src/@types"], "lib": [ "es2019", "dom" diff --git a/yarn.lock b/yarn.lock index f78a787..173b1bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -839,25 +839,10 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*": - version "18.0.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a" - integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA== - -"@types/node@16.9.1": - version "16.9.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708" - integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g== - -"@types/node@^16.11.26": - version "16.11.41" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.41.tgz#88eb485b1bfdb4c224d878b7832239536aa2f813" - integrity sha512-mqoYK2TnVjdkGk8qXAVGc/x9nSaTpSrFaGFm43BUH3IdoBV0nta6hYaGmdOvIMlbHJbUEVen3gvwpwovAZKNdQ== - -"@types/node@^17.0.12": - version "17.0.45" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" - integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== +"@types/node@*", "@types/node@16.11.38", "@types/node@16.9.1", "@types/node@^16.11.26", "@types/node@^17.0.12": + version "16.11.38" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.38.tgz#be0edd097b23eace6c471c525a74b3f98803017f" + integrity sha512-hjO/0K140An3GWDw2HJfq7gko3wWeznbjXgg+rzPdVzhe198hp4x2i1dgveAOEiFKd8sOilAxzoSJiVv5P/CUg== "@types/npm-package-arg@*": version "6.1.1" @@ -1059,12 +1044,19 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + ajv-keywords@^3.4.1: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4: +ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.3, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1074,7 +1066,7 @@ ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.1: +ajv@^8.0.0, ajv@^8.0.1, ajv@^8.6.3: version "8.11.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== @@ -1371,7 +1363,7 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -atomically@^1.3.1: +atomically@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe" integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w== @@ -1851,21 +1843,21 @@ concat-stream@^1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" -conf@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/conf/-/conf-7.1.2.tgz#d9678a9d8f04de8bf5cd475105da8fdae49c2ec4" - integrity sha512-r8/HEoWPFn4CztjhMJaWNAe5n+gPUCSaJ0oufbqDLFKsA1V8JjAG7G+p0pgoDFAws9Bpk2VtVLLXqOBA7WxLeg== +conf@^10.1.2: + version "10.1.2" + resolved "https://registry.yarnpkg.com/conf/-/conf-10.1.2.tgz#50132158f388756fa9dea3048f6b47935315c14e" + integrity sha512-o9Fv1Mv+6A0JpoayQ8JleNp3hhkbOJP/Re/Q+QqxMPHPkABVsRjQGWZn9A5GcqLiTNC6d89p2PB5ZhHVDSMwyg== dependencies: - ajv "^6.12.2" - atomically "^1.3.1" + ajv "^8.6.3" + ajv-formats "^2.1.1" + atomically "^1.7.0" debounce-fn "^4.0.0" - dot-prop "^5.2.0" - env-paths "^2.2.0" + dot-prop "^6.0.1" + env-paths "^2.2.1" json-schema-typed "^7.0.3" - make-dir "^3.1.0" - onetime "^5.1.0" + onetime "^5.1.2" pkg-up "^3.1.0" - semver "^7.3.2" + semver "^7.3.5" config-chain@^1.1.11: version "1.1.13" @@ -2141,6 +2133,13 @@ dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== + dependencies: + is-obj "^2.0.0" + dotenv-expand@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" @@ -2257,13 +2256,13 @@ electron-publish@22.14.13: lazy-val "^1.0.5" mime "^2.5.2" -electron-store@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-6.0.1.tgz#2178b9dc37aeb749d99cf9d1d1bc090890b922dc" - integrity sha512-8rdM0XEmDGsLuZM2oRABzsLX+XmD5x3rwxPMEPv0MrN9/BWanyy3ilb2v+tCrKtIZVF3MxUiZ9Bfqe8e0popKQ== +electron-store@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.0.2.tgz#95c8cf81c1e1cf48b24f3ceeea24b921c1ff62d7" + integrity sha512-9GwUMv51w8ydbkaG7X0HrPlElXLApg63zYy1/VZ/a08ndl0gfm4iCoD3f0E1JvP3V16a+7KxqriCI0c122stiA== dependencies: - conf "^7.1.2" - type-fest "^0.16.0" + conf "^10.1.2" + type-fest "^2.12.2" electron-window-state@^5.0.3: version "5.0.3" @@ -2313,7 +2312,7 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" -env-paths@^2.2.0: +env-paths@^2.2.0, env-paths@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== @@ -3785,7 +3784,7 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" -make-dir@^3.0.0, make-dir@^3.1.0: +make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -4272,7 +4271,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.0: +onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -5314,11 +5313,6 @@ type-fest@^0.13.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== -type-fest@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" - integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -5329,6 +5323,11 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^2.12.2: + version "2.13.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.13.0.tgz#d1ecee38af29eb2e863b22299a3d68ef30d2abfb" + integrity sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw== + type@^1.0.1: version "1.2.0" resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"