/** * extension/background.js — Manifest V3 service worker. * * Responsibilities: * - Update the action badge (number of matching vault items) for the active tab. * - Bridge SAVE_CREDENTIALS messages from content script → chrome.storage.local. * - Re-update badges when the vault cache changes. * - Lock the vault automatically after IDLE_LOCK_SECONDS of system inactivity. */ // ── Idle lock ───────────────────────────────────────────────────────────────── // Default: 10 minutes. User can change via the Account view in the popup. const DEFAULT_IDLE_LOCK_SECONDS = 600; const IDLE_TIMEOUT_KEY = "idle_lock_seconds"; /** Apply the idle detection interval, reading the user's saved preference. */ async function applyIdleInterval() { const stored = await chrome.storage.local.get(IDLE_TIMEOUT_KEY); const seconds = stored[IDLE_TIMEOUT_KEY] ?? DEFAULT_IDLE_LOCK_SECONDS; if (seconds === 0) { // "Never" — unregister by setting to Chrome's maximum (the API requires a value). chrome.idle.setDetectionInterval(3600); } else { chrome.idle.setDetectionInterval(Math.max(15, seconds)); } console.log( "[PassKeeper] Idle lock interval:", seconds === 0 ? "never" : seconds + "s", ); } // Apply on every SW startup (SW can be killed and restarted at any time). applyIdleInterval(); chrome.idle.onStateChanged.addListener(async (newState) => { if (newState === "idle" || newState === "locked") { // Check if the user set "Never" before locking. const stored = await chrome.storage.local.get(IDLE_TIMEOUT_KEY); if ((stored[IDLE_TIMEOUT_KEY] ?? DEFAULT_IDLE_LOCK_SECONDS) === 0) return; console.log("[PassKeeper] System", newState, "— locking vault."); await chrome.storage.session.clear(); await chrome.storage.local.remove("vault_items_cs"); const tabs = await chrome.tabs.query({}); tabs.forEach((tab) => { if (tab.id) chrome.action.setBadgeText({ text: "", tabId: tab.id }); }); } }); // ── Badge helpers ──────────────────────────────────────────────────────────── async function updateBadgeForTab(tabId, url) { try { const { vault_items } = await chrome.storage.session.get("vault_items"); if (!vault_items?.length) { chrome.action.setBadgeText({ text: "", tabId }); return; } let hostname; try { hostname = new URL(url).hostname.replace(/^www\./, ""); } catch { chrome.action.setBadgeText({ text: "", tabId }); return; } const matches = vault_items.filter((item) => { if (item.item_type !== "password" || !item.plain?.url) return false; try { const h = new URL(item.plain.url).hostname.replace(/^www\./, ""); return ( h === hostname || h.endsWith(`.${hostname}`) || hostname.endsWith(`.${h}`) ); } catch { return false; } }); if (matches.length > 0) { chrome.action.setBadgeText({ text: String(matches.length), tabId }); chrome.action.setBadgeBackgroundColor({ color: "#1a73e8", tabId }); } else { chrome.action.setBadgeText({ text: "", tabId }); } } catch { /* tab may have closed */ } } async function refreshAllBadges() { const tabs = await chrome.tabs.query({}); for (const tab of tabs) { if (tab.url?.startsWith("http")) updateBadgeForTab(tab.id, tab.url); } } // ── Tab events ─────────────────────────────────────────────────────────────── chrome.tabs.onActivated.addListener(async ({ tabId }) => { try { const tab = await chrome.tabs.get(tabId); if (tab.url?.startsWith("http")) updateBadgeForTab(tabId, tab.url); } catch {} }); chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (changeInfo.status === "complete" && tab.url?.startsWith("http")) { updateBadgeForTab(tabId, tab.url); } }); // ── Message handler ────────────────────────────────────────────────────────── chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // Content script detected a login form on the page — no action needed in the // background, but we must respond to close the message port cleanly. if (msg.type === "FORMS_DETECTED") { sendResponse({ ok: true }); return false; } // Popup updated the idle lock timeout → re-apply immediately. if (msg.type === "SET_IDLE_TIMEOUT") { applyIdleInterval(); sendResponse({ ok: true }); return false; } // Content script requests opening the vault tab (from suggestion dropdown). if (msg.type === "OPEN_VAULT") { chrome.tabs.create({ url: "https://pwkeeper.ngodanguyen.tech/vault" }); sendResponse({ ok: true }); return false; } // Content script requests navigating the popup to the generator view. if (msg.type === "OPEN_GENERATOR") { chrome.storage.session.set({ popup_nav: "generator" }); chrome.action.openPopup().catch(() => { // openPopup() requires user gesture in some Chrome versions — fallback is a no-op. }); sendResponse({ ok: true }); return false; } // No-op ping from popup — keeps the service worker alive while the browser // is open so chrome.storage.session is not wiped between popup openings. if (msg.type === "KEEPALIVE") { sendResponse({ ok: true }); return false; } // Popup signals vault cache was refreshed → re-check badges AND forward // the decrypted items directly to all content scripts (avoids storage read). if (msg.type === "VAULT_UPDATED") { refreshAllBadges(); const payload = { type: "VAULT_UPDATED", vault_items: msg.vault_items || [], }; chrome.tabs.query({}, function (tabs) { tabs.forEach(function (tab) { if ( tab.url && (tab.url.startsWith("http://") || tab.url.startsWith("https://")) ) { chrome.tabs.sendMessage(tab.id, payload).catch(function () {}); } }); }); sendResponse({ ok: true }); return false; } // Content script detected credentials on form submit → store for save-prompt. if (msg.type === "SAVE_CREDENTIALS") { // Store in local storage so the prompt survives service worker restarts // and is guaranteed to be present when the user next opens the popup. chrome.storage.local.set({ pending_save: msg.data }); sendResponse({ ok: true }); return false; } // Bridge: web app logged in → store tokens in extension storage. if (msg.type === "WEB_SESSION_SYNC") { chrome.storage.session.set({ access_token: msg.access_token, enc_key_salt: msg.enc_key_salt || "", }); // Also persist enc_key_salt locally so the unlock-only view survives browser restarts chrome.storage.local.set({ refresh_token: msg.refresh_token, ...(msg.enc_key_salt ? { enc_key_salt: msg.enc_key_salt } : {}), }); sendResponse({ ok: true }); return false; } // Bridge: web app logged out → clear extension session. if (msg.type === "WEB_SESSION_CLEAR") { chrome.storage.session.remove([ "access_token", "vault_key_jwk", "vault_items", "enc_key_salt", ]); chrome.storage.local.remove("refresh_token"); sendResponse({ ok: true }); return false; } // Popup logged in via extension → inject session into open vault tabs. if (msg.type === "EXT_SESSION_SYNC") { chrome.tabs.query( { url: "https://pwkeeper.ngodanguyen.tech/*" }, (tabs) => { tabs.forEach((tab) => { chrome.tabs .sendMessage(tab.id, { type: "INJECT_SESSION", access_token: msg.access_token, refresh_token: msg.refresh_token, enc_key_salt: msg.enc_key_salt, }) .catch(() => {}); }); }, ); sendResponse({ ok: true }); return false; } return false; });