src/main/index.ts
Rama main · réplica de lectura del repositorio original.
import { app, BrowserWindow } from 'electron'
import { installAppMenu } from './app-menu'
import { clearClipboardIfOurs } from './clipboard-guard'
import { installCrashHandlers } from './crash-handlers'
import { extractNvxArgv, resolveOpenFilePath, setValidatedPending } from './file-opener'
import { closeVaultDrained, stopAutoLockTimer } from './ipc-handlers'
import { logError } from './log'
import { installNavigationGuard } from './navigation-guard'
import { takePendingVault } from './pending-vault'
import { installPermissionGuard } from './permission-guard'
import { hardenProcess } from './process-hardening'
import { cleanupOrphanedTempDbs } from './vault/container'
import { initSodium } from './vault/crypto'
import { createWindow } from './window'
// ─── Dev/prod isolation ─────────────────────────────────────────────────────────
// Must run before requestSingleInstanceLock() and any getPath('userData') access,
// since both are derived from the app name.
if (!app.isPackaged) {
app.setName('Notvex Dev')
}
// After setName, so a crash is logged under the right userData.
installCrashHandlers()
// Before any key exists. Dev builds skip it: the debugger has to attach.
if (app.isPackaged && !hardenProcess()) {
app.quit()
process.exit(1)
}
// Fuses don't cover these Chromium switches: they expose renderers over CDP, launch child
// processes through a caller-chosen command, or undo enableSandbox(). Chromium has already
// parsed them by now, so exit before any window exists.
const blockedSwitches = [
'remote-debugging-port',
'remote-debugging-pipe',
'renderer-cmd-prefix',
'gpu-launcher',
'utility-cmd-prefix',
'browser-subprocess-path',
'no-sandbox'
]
if (app.isPackaged && blockedSwitches.some((s) => app.commandLine.hasSwitch(s))) {
app.quit()
process.exit(1)
}
// Must run before ready. Sandboxes every renderer regardless of its webPreferences.
app.enableSandbox()
// Before any webContents exists, so none starts unguarded.
installNavigationGuard()
// ─── Single-instance lock ──────────────────────────────────────────────────────
if (!app.requestSingleInstanceLock()) {
app.quit()
process.exit(0)
}
let mainWindow: BrowserWindow | null = null
let isQuitting = false
let readyToQuit = false
// Not app.isReady(): open-file can also arrive while initSodium() is still
// pending, and opening a window there would run before sodium is ready and
// leave startup to open a second one.
let startupDone = false
function openMainWindow(): void {
const win = createWindow(takePendingVault)
// On macOS the app outlives its last window; open-file / second-instance
// must not reuse the destroyed one.
win.on('closed', (): void => {
if (mainWindow === win) mainWindow = null
})
mainWindow = win
}
void app.whenReady().then(async (): Promise<void> => {
cleanupOrphanedTempDbs()
await initSodium()
installPermissionGuard()
installAppMenu()
openMainWindow()
startupDone = true
app.on('activate', (): void => {
if (BrowserWindow.getAllWindows().length === 0) openMainWindow()
})
})
app.on('window-all-closed', (): void => {
if (process.platform === 'darwin') {
// macOS keeps the app alive with no window, so lock here (see closeVaultDrained()).
stopAutoLockTimer()
void closeVaultDrained(null)
return
}
app.quit() // before-quit does the real shutdown
})
// Primary drain for Cmd+Q / app.quit(): before-quit fires before any window's 'close'
// (see closeVaultDrained()).
app.on('before-quit', (event): void => {
if (readyToQuit) return // our own re-entrant app.quit(): let it through
event.preventDefault() // synchronous, before any await
if (isQuitting) return // a duplicate quit signal while one is already in flight
isQuitting = true
void (async (): Promise<void> => {
try {
stopAutoLockTimer()
await closeVaultDrained(mainWindow)
} catch (e) {
logError('[quit] closeVault failed:', e)
} finally {
await clearClipboardIfOurs('quit')
readyToQuit = true
app.quit() // re-enters before-quit, which now lets it through
}
})()
})
app.on('second-instance', (_event, argv): void => {
const filePath = extractNvxArgv(argv)
// Same as open-file: no window means store the path and, once started,
// open a window for it — the user relaunched the app either way.
if (!mainWindow) {
if (filePath) setValidatedPending(filePath)
if (startupDone && !isQuitting) openMainWindow()
return
}
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.focus()
if (filePath) {
resolveOpenFilePath(mainWindow, filePath).catch((e) => {
logError('[open-file] resolveOpenFilePath failed:', e)
})
}
})
app.on('open-file', (event, filePath): void => {
event.preventDefault()
if (mainWindow) {
resolveOpenFilePath(mainWindow, filePath).catch((e) => {
logError('[open-file] resolveOpenFilePath failed:', e)
})
return
}
// No window yet (cold start) or anymore (macOS dock): store the path; the
// window's renderer picks it up via takePendingVault on load. No new
// window mid-quit: it would re-arm the auto-lock timer before-quit stopped.
setValidatedPending(filePath)
if (startupDone && !isQuitting) openMainWindow()
})