/** * 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.session * so the popup can pick them up as a save-prompt. * - Re-update badges when the vault cache changes. */ // ── 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) => { // 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 all tab badges. if (msg.type === 'VAULT_UPDATED') { refreshAllBadges(); sendResponse({ ok: true }); return false; } // Content script detected credentials on form submit → store for save-prompt. if (msg.type === 'SAVE_CREDENTIALS') { chrome.storage.session.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; });