From 5a6db9bd4842be0a64a0a586cdabefc47459d6c1 Mon Sep 17 00:00:00 2001 From: talksik Date: Sun, 12 Apr 2026 13:30:18 -0700 Subject: [PATCH] fix: harden production electron app behavior - No developer tools or zoom - No refresh window - Open links externally --- js/src/main.ts | 73 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/js/src/main.ts b/js/src/main.ts index 0ee644b..3db9d71 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -5,12 +5,14 @@ import { updateElectronApp, UpdateSourceType } from 'update-electron-app'; import type { LinkMetadata } from './lib/link-metadata'; -updateElectronApp({ - updateSource: { - type: UpdateSourceType.StaticStorage, - baseUrl: `https://storage.googleapis.com/flowy-releases/llink/${process.platform}/${process.arch}` - } -}); +if (app.isPackaged) { + updateElectronApp({ + updateSource: { + type: UpdateSourceType.StaticStorage, + baseUrl: `https://storage.googleapis.com/flowy-releases/llink/${process.platform}/${process.arch}` + } + }); +} // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (started) { @@ -27,6 +29,61 @@ let autoplayWindow: BrowserWindow | null = null; let huddleWindow: BrowserWindow | null = null; let screenRecordWindow: BrowserWindow | null = null; +function hardenWindow(win: BrowserWindow) { + const wc = win.webContents; + + // Disable pinch / Cmd+scroll zoom; our layouts assume DPR=1. + wc.setVisualZoomLevelLimits(1, 1); + + // Block devtools, reload, and zoom keyboard shortcuts in packaged builds. + wc.on('before-input-event', (event, input) => { + if (input.type !== 'keyDown') return; + const key = input.key.toLowerCase(); + const cmdOrCtrl = input.meta || input.control; + + const isDevtools = + key === 'f12' || + (cmdOrCtrl && input.alt && key === 'i') || + (cmdOrCtrl && input.shift && key === 'i') || + (cmdOrCtrl && input.shift && key === 'c') || + (cmdOrCtrl && input.shift && key === 'j'); + const isReload = cmdOrCtrl && (key === 'r' || key === 'f5'); + const isZoom = cmdOrCtrl && (key === '=' || key === '+' || key === '-' || key === '0'); + + if (app.isPackaged && (isDevtools || isReload)) { + event.preventDefault(); + } + if (isZoom) { + event.preventDefault(); + } + }); + + // Route window.open / target=_blank through the OS browser instead of a new BrowserWindow. + wc.setWindowOpenHandler(({ url }) => { + if (url.startsWith('http://') || url.startsWith('https://')) { + shell.openExternal(url); + } + return { action: 'deny' }; + }); + + // Freeze the window to its initial origin — block any full-page navigation. + // In-app routing uses pushState / hash which don't trigger will-navigate. + wc.on('will-navigate', (event, url) => { + const currentUrl = wc.getURL(); + if (!currentUrl) return; + try { + if (new URL(url).origin !== new URL(currentUrl).origin) { + event.preventDefault(); + if (url.startsWith('http://') || url.startsWith('https://')) { + shell.openExternal(url); + } + } + } catch { + event.preventDefault(); + } + }); +} + const createWindow = () => { mainWindow = new BrowserWindow({ width: 800, @@ -39,6 +96,7 @@ const createWindow = () => { backgroundThrottling: false, }, }); + hardenWindow(mainWindow); if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); @@ -72,6 +130,7 @@ const createAutoplayWindow = () => { }, }); autoplayWindow.setVisibleOnAllWorkspaces(true); + hardenWindow(autoplayWindow); if (AUTOPLAY_WINDOW_VITE_DEV_SERVER_URL) { autoplayWindow.loadURL(AUTOPLAY_WINDOW_VITE_DEV_SERVER_URL); @@ -103,6 +162,7 @@ const createHuddleWindow = () => { preload: path.join(__dirname, 'preload.js'), }, }); + hardenWindow(huddleWindow); if (HUDDLE_WINDOW_VITE_DEV_SERVER_URL) { huddleWindow.loadURL(HUDDLE_WINDOW_VITE_DEV_SERVER_URL); @@ -147,6 +207,7 @@ const createScreenRecordWindow = () => { }, }); screenRecordWindow.setVisibleOnAllWorkspaces(true); + hardenWindow(screenRecordWindow); if (SCREEN_RECORD_WINDOW_VITE_DEV_SERVER_URL) { screenRecordWindow.loadURL(SCREEN_RECORD_WINDOW_VITE_DEV_SERVER_URL);