Update browser extension autolock timeout handler to use alarm API for >30s timeouts (#1684)

This commit is contained in:
Leendert de Borst
2026-02-13 14:17:44 +01:00
committed by Leendert de Borst
parent ba744b8e93
commit 50e73c08f2
3 changed files with 175 additions and 55 deletions

View File

@@ -4,7 +4,7 @@
import { onMessage, sendMessage } from "webext-bridge/background";
import { handleResetAutoLockTimer, handlePopupHeartbeat, handleSetAutoLockTimeout } from '@/entrypoints/background/AutolockTimeoutHandler';
import { handleResetAutoLockTimer, handlePopupHeartbeat, handleSetAutoLockTimeout, initializeAutoLockAlarm, handleAutoLockAlarm } from '@/entrypoints/background/AutolockTimeoutHandler';
import { handleClipboardCopied, handleCancelClipboardClear, handleGetClipboardClearTimeout, handleSetClipboardClearTimeout, handleGetClipboardCountdownState } from '@/entrypoints/background/ClipboardClearHandler';
import { setupContextMenus } from '@/entrypoints/background/ContextMenu';
import { handleGetWebAuthnSettings, handleWebAuthnCreate, handleWebAuthnGet, handlePasskeyPopupResponse, handleGetRequestData } from '@/entrypoints/background/PasskeyHandler';
@@ -89,6 +89,15 @@ export default defineBackground({
await setupContextMenus();
}
/*
* Initialize auto-lock alarm system.
* This ensures the alarm is restored if the service worker was terminated.
*/
await initializeAutoLockAlarm();
// Register alarm listener for auto-lock
browser.alarms.onAlarm.addListener(handleAutoLockAlarm);
// Listen for custom commands
try {
browser.commands.onCommand.addListener(async (command) => {

View File

@@ -1,78 +1,170 @@
import { storage } from 'wxt/utils/storage';
import { handleLockVault } from '@/entrypoints/background/VaultMessageHandler';
import { LocalPreferencesService } from '@/utils/LocalPreferencesService';
let autoLockTimer: NodeJS.Timeout | null = null;
import type { Browser } from 'wxt/browser';
import { browser, storage } from '#imports';
const AUTO_LOCK_ALARM_NAME = 'vault-auto-lock';
/*
* Threshold in seconds below which we use setTimeout instead of alarms.
* Alarms have a minimum delay of 30 seconds in production (packed) extensions.
* For short timeouts, setTimeout is more accurate and the service worker
* won't terminate before the timer fires.
*/
const SHORT_TIMEOUT_THRESHOLD = 30;
// Timer handle for short timeouts using setTimeout
let shortTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
/**
* Reset the auto-lock timer.
* Lock the vault due to inactivity timeout.
*/
export function handleResetAutoLockTimer(): void {
resetAutoLockTimer();
async function lockVaultDueToInactivity(): Promise<void> {
// Check if vault is still unlocked before locking
const encryptionKey = await storage.getItem('session:encryptionKey') as string | null;
if (!encryptionKey) {
// Vault is already locked
return;
}
try {
handleLockVault();
console.info('[AUTO_LOCK] Vault locked due to inactivity');
} catch (error) {
console.error('[AUTO_LOCK] Error locking vault:', error);
}
}
/**
* Handle popup heartbeat - extend auto-lock timer.
* Clear the short timeout timer if it exists.
*/
export function handlePopupHeartbeat(): void {
extendAutoLockTimer();
function clearShortTimeoutTimer(): void {
if (shortTimeoutTimer) {
clearTimeout(shortTimeoutTimer);
shortTimeoutTimer = null;
}
}
/**
* Set the auto-lock timeout setting.
* Set the auto-lock timer using the appropriate method based on timeout duration.
* Uses setTimeout for short timeouts (< 30s) and alarms for longer ones.
*/
export async function handleSetAutoLockTimeout(timeout: number): Promise<boolean> {
await LocalPreferencesService.setAutoLockTimeout(timeout);
resetAutoLockTimer();
return true;
async function setAutoLockTimer(timeoutSeconds: number): Promise<void> {
// Clear any existing timers
clearShortTimeoutTimer();
await browser.alarms.clear(AUTO_LOCK_ALARM_NAME);
if (timeoutSeconds < SHORT_TIMEOUT_THRESHOLD) {
/*
* Use setTimeout for short timeouts.
* Service worker won't terminate before the timer fires.
*/
shortTimeoutTimer = setTimeout(() => {
shortTimeoutTimer = null;
lockVaultDueToInactivity();
}, timeoutSeconds * 1000);
} else {
/*
* Use alarms for longer timeouts.
* Alarms persist across service worker restarts.
*/
const delayInMinutes = timeoutSeconds / 60;
await browser.alarms.create(AUTO_LOCK_ALARM_NAME, {
delayInMinutes: delayInMinutes
});
}
}
/**
* Reset the auto-lock timer based on current settings.
* Initialize the auto-lock alarm system.
* This should be called when the background script starts.
* It checks if the vault is unlocked and if so, ensures a timer is set.
*/
async function resetAutoLockTimer(): Promise<void> {
// Clear existing timer
if (autoLockTimer) {
clearTimeout(autoLockTimer);
autoLockTimer = null;
export async function initializeAutoLockAlarm(): Promise<void> {
// Check if vault is unlocked
const encryptionKey = await storage.getItem('session:encryptionKey') as string | null;
if (!encryptionKey) {
// Vault is locked, clear any existing alarm
clearShortTimeoutTimer();
await browser.alarms.clear(AUTO_LOCK_ALARM_NAME);
return;
}
// Get timeout setting
const timeout = await LocalPreferencesService.getAutoLockTimeout();
// Don't set timer if timeout is 0 (disabled) or if vault is already locked
if (timeout === 0) {
// Auto-lock disabled, clear any existing alarm
clearShortTimeoutTimer();
await browser.alarms.clear(AUTO_LOCK_ALARM_NAME);
return;
}
/*
* For short timeouts, we can't restore the exact remaining time after
* service worker restart, so we just set a new timer with the full duration.
* For alarms, check if one already exists to avoid resetting the countdown.
*/
if (timeout < SHORT_TIMEOUT_THRESHOLD) {
// Short timeout - set a new setTimeout (can't persist across restarts anyway)
if (!shortTimeoutTimer) {
await setAutoLockTimer(timeout);
}
} else {
// Long timeout - only create alarm if one doesn't exist
const existingAlarm = await browser.alarms.get(AUTO_LOCK_ALARM_NAME);
if (!existingAlarm) {
await setAutoLockTimer(timeout);
}
}
}
/**
* Handle the auto-lock alarm firing.
* This is called by the alarm listener in background.ts.
*/
export async function handleAutoLockAlarm(alarm: Browser.alarms.Alarm): Promise<void> {
if (alarm.name !== AUTO_LOCK_ALARM_NAME) {
return;
}
await lockVaultDueToInactivity();
}
/**
* Reset the auto-lock timer.
* This clears any existing timer and creates a new one with the full timeout period.
*/
export async function handleResetAutoLockTimer(): Promise<void> {
// Get timeout setting
const timeout = await LocalPreferencesService.getAutoLockTimeout();
// Don't set timer if timeout is 0 (disabled)
if (timeout === 0) {
clearShortTimeoutTimer();
await browser.alarms.clear(AUTO_LOCK_ALARM_NAME);
return;
}
// Check if vault is unlocked before setting timer
const encryptionKey = await storage.getItem('session:encryptionKey') as string | null;
if (!encryptionKey) {
// Vault is already locked, don't start timer
clearShortTimeoutTimer();
await browser.alarms.clear(AUTO_LOCK_ALARM_NAME);
return;
}
// Set new timer
autoLockTimer = setTimeout(async () => {
try {
handleLockVault();
console.info('[AUTO_LOCK] Vault locked due to inactivity');
autoLockTimer = null;
} catch (error) {
console.error('[AUTO_LOCK] Error locking vault:', error);
}
}, timeout * 1000);
await setAutoLockTimer(timeout);
}
/**
* Extend the auto-lock timer by the full timeout period.
* This is called by popup heartbeats to prevent locking while popup is active.
* Handle popup heartbeat - extend auto-lock timer.
* This resets the timer to prevent locking while popup is active.
*/
async function extendAutoLockTimer(): Promise<void> {
export async function handlePopupHeartbeat(): Promise<void> {
// Get timeout setting
const timeout = await LocalPreferencesService.getAutoLockTimeout();
@@ -83,28 +175,46 @@ async function extendAutoLockTimer(): Promise<void> {
// Check if vault is unlocked
const encryptionKey = await storage.getItem('session:encryptionKey') as string | null;
if (!encryptionKey) {
// Vault is already locked, don't extend timer
return;
}
// Clear existing timer and start a new one
if (autoLockTimer) {
clearTimeout(autoLockTimer);
autoLockTimer = null;
await setAutoLockTimer(timeout);
}
/**
* Set the auto-lock timeout setting.
* Updates the stored preference and resets the timer with the new value.
*/
export async function handleSetAutoLockTimeout(timeout: number): Promise<boolean> {
await LocalPreferencesService.setAutoLockTimeout(timeout);
// Clear existing timers
clearShortTimeoutTimer();
await browser.alarms.clear(AUTO_LOCK_ALARM_NAME);
// If timeout is 0 (disabled), we're done
if (timeout === 0) {
return true;
}
// Set new timer
autoLockTimer = setTimeout(async () => {
try {
// Lock the vault (preserves local data for offline unlock)
handleLockVault();
// Check if vault is unlocked before setting new timer
const encryptionKey = await storage.getItem('session:encryptionKey') as string | null;
if (!encryptionKey) {
// Vault is locked, don't start timer
return true;
}
console.info('[AUTO_LOCK] Vault locked due to inactivity');
autoLockTimer = null;
} catch (error) {
console.error('[AUTO_LOCK] Error locking vault:', error);
}
}, timeout * 1000);
await setAutoLockTimer(timeout);
return true;
}
/**
* Clear the auto-lock alarm and any short timeout timer.
* This should be called when the vault is locked.
*/
export async function clearAutoLockAlarm(): Promise<void> {
clearShortTimeoutTimer();
await browser.alarms.clear(AUTO_LOCK_ALARM_NAME);
}

View File

@@ -10,7 +10,8 @@ export default defineConfig({
"activeTab",
"contextMenus",
"scripting",
"clipboardWrite"
"clipboardWrite",
"alarms"
];
// Only add offscreen permission for Chrome and Edge