import { BrowserWindow, ipcMain, shell } from 'electron'; import { createServer, Server } from 'http'; import { IPC } from './shared-with-frontend/ipc-events.const'; import { OAUTH_LOOPBACK_PORT_MIN, OAUTH_LOOPBACK_PORT_MAX, } from './shared-with-frontend/oauth-loopback.const'; import { log } from 'electron-log/main'; const LOOPBACK_HOST = '127.0.0.1'; const OAUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes let loopbackServer: Server | null = null; let oauthTimeoutId: ReturnType | null = null; const cleanupServer = (): void => { if (oauthTimeoutId) { clearTimeout(oauthTimeoutId); oauthTimeoutId = null; } if (loopbackServer) { loopbackServer.close(); loopbackServer = null; } }; // Success page shown in the user's browser after completing OAuth const SUCCESS_HTML = ` Super Productivity

Authentication complete

You can close this tab and return to Super Productivity.

`; export const initPluginOAuth = (mainWin: BrowserWindow): void => { // Prepare: start a loopback HTTP server and return the port. // Google Desktop OAuth requires http://127.0.0.1: redirect URIs // and blocks embedded webviews, so we open the system browser instead. ipcMain.handle( IPC.PLUGIN_OAUTH_PREPARE, async (_event, requestedPort?: number): Promise<{ port: number }> => { cleanupServer(); return new Promise<{ port: number }>((resolve, reject) => { let handled = false; let port = 0; if (requestedPort !== undefined) { if ( !Number.isInteger(requestedPort) || requestedPort < OAUTH_LOOPBACK_PORT_MIN || requestedPort > OAUTH_LOOPBACK_PORT_MAX ) { reject( new Error( `Invalid OAuth loopback port ${requestedPort}; must be an integer in [${OAUTH_LOOPBACK_PORT_MIN}, ${OAUTH_LOOPBACK_PORT_MAX}].`, ), ); return; } port = requestedPort; } const server = createServer((req, res) => { if (handled) { // eslint-disable-next-line @typescript-eslint/naming-convention res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(SUCCESS_HTML); return; } handled = true; const url = new URL(req.url!, `http://${LOOPBACK_HOST}`); const code = url.searchParams.get('code'); const error = url.searchParams.get('error'); const state = url.searchParams.get('state'); // eslint-disable-next-line @typescript-eslint/naming-convention res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(SUCCESS_HTML); mainWin.webContents.send(IPC.PLUGIN_OAUTH_CB, { code, error, state }); // Re-focus the main window after auth completes if (!mainWin.isDestroyed()) { mainWin.show(); mainWin.focus(); } cleanupServer(); }); server.on('error', (err) => { cleanupServer(); // Note: EADDRINUSE message interpolates port which is 0 only in the system-assigned // (no requested port) case — where EADDRINUSE effectively cannot occur. if ((err as NodeJS.ErrnoException).code === 'EADDRINUSE') { reject( new Error( `OAuth loopback port ${port} is already in use. Close the app using it and try again.`, ), ); } else { reject(err); } }); server.listen(port, LOOPBACK_HOST, () => { const addr = server.address(); if (addr && typeof addr !== 'string') { loopbackServer = server; oauthTimeoutId = setTimeout(() => { log('Plugin OAuth: Timeout – closing abandoned loopback server'); cleanupServer(); }, OAUTH_TIMEOUT_MS); log(`Plugin OAuth: Loopback server listening on port ${addr.port}`); resolve({ port: addr.port }); } else { server.close(); reject(new Error('Failed to start OAuth loopback server')); } }); }); }, ); // Open the auth URL in the system browser (not an embedded webview). // Google blocks OAuth in embedded browsers (Electron BrowserWindow). ipcMain.on(IPC.PLUGIN_OAUTH_START, (_ev: unknown, { url }: { url: string }) => { log('Plugin OAuth: Opening system browser for auth'); // Validate URL protocol before opening to prevent file:// or javascript: abuse. // Echo back the state param so the renderer can match the error to the // pending flow (state validation in handleRedirectError). let state: string | undefined; try { const parsed = new URL(url); state = parsed.searchParams.get('state') ?? undefined; if (parsed.protocol !== 'https:') { log('Plugin OAuth: Rejected non-https auth URL:', parsed.protocol); mainWin.webContents.send(IPC.PLUGIN_OAUTH_CB, { error: 'invalid_auth_url', state, }); cleanupServer(); return; } } catch { mainWin.webContents.send(IPC.PLUGIN_OAUTH_CB, { error: 'invalid_auth_url', state, }); cleanupServer(); return; } shell.openExternal(url).catch((err: unknown) => { log('Plugin OAuth: Failed to open system browser:', err); mainWin.webContents.send(IPC.PLUGIN_OAUTH_CB, { error: 'failed_to_open_browser', state, }); cleanupServer(); }); }); };