diff --git a/extension/background.js b/extension/background.js index 8e6d0b8..ebb6a7a 100644 --- a/extension/background.js +++ b/extension/background.js @@ -65,6 +65,13 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 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(); diff --git a/extension/content/content.js b/extension/content/content.js index 89e0451..f10dc19 100644 --- a/extension/content/content.js +++ b/extension/content/content.js @@ -227,9 +227,60 @@ injectAutofillButtons(matching); } + // ── Duplicate detection ─────────────────────────────────────────────────────── + + /** + * Checks the cached vault to determine whether the submitted credentials are + * new or have been updated since last saved. + * + * Returns: + * 'new' — no item found for this site; definitely show the banner. + * 'updated' — item exists but username/password differs; show update banner. + * 'same' — credentials are identical to a stored item; suppress the banner. + */ + async function classifyCredentials(username, password) { + let vault_items; + try { + ({ vault_items } = await chrome.storage.session.get('vault_items')); + } catch { + // If we can't read storage (e.g. extension context invalidated), show banner. + return 'new'; + } + if (!vault_items?.length) return 'new'; + + const host = location.hostname.replace(/^www\./, ''); + + const siteItems = 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 === host || h.endsWith(`.${host}`) || host.endsWith(`.${h}`); + } catch { return false; } + }); + + if (!siteItems.length) return 'new'; + + // Check for exact match (same username AND same password). + const exactMatch = siteItems.some( + item => item.plain?.username === username && item.plain?.password === password + ); + if (exactMatch) return 'same'; + + // Credentials differ → treat as updated. + return 'updated'; + } + // ── Auto-save banner ────────────────────────────────────────────────────────── - function showSaveBanner(username, password) { + /** + * Shows the save/update banner. The banner stays visible until the user + * explicitly clicks "Save" or "Not now" — there is NO auto-dismiss timeout. + * + * @param {string} username + * @param {string} password + * @param {'new'|'updated'} credentialState — controls the title copy. + */ + function showSaveBanner(username, password, credentialState) { if (_bannerEl) _bannerEl.remove(); const banner = document.createElement('div'); @@ -251,13 +302,16 @@ minWidth: '240px', }); - const site = escHtml(location.hostname); - const user = escHtml(username); + const site = escHtml(location.hostname); + const user = escHtml(username); + const title = credentialState === 'updated' + ? 'Update in PassKeeper?' + : 'Save to PassKeeper?'; banner.innerHTML = `
@@ -271,24 +325,24 @@ document.body.appendChild(banner); _bannerEl = banner; + // No setTimeout — banner stays until the user makes an explicit choice. const dismiss = () => { if (_bannerEl === banner) { banner.remove(); _bannerEl = null; } }; banner.querySelector('#__pk_close__').addEventListener('click', dismiss); banner.querySelector('#__pk_skip__').addEventListener('click', dismiss); banner.querySelector('#__pk_save__').addEventListener('click', () => { + console.log('[PassKeeper] User chose to save credentials for', location.hostname); chrome.runtime.sendMessage({ type: 'SAVE_CREDENTIALS', data: { url: location.href, siteName: document.title || location.hostname, username, password }, }).catch(() => {}); dismiss(); }); - - setTimeout(dismiss, 20_000); } // ── Form submission watch ───────────────────────────────────────────────────── function watchSubmissions() { - document.addEventListener('submit', e => { + document.addEventListener('submit', async e => { const form = e.target; const pwField = form.querySelector('input[type="password"]:not([disabled])'); if (!pwField?.value) return; @@ -300,9 +354,18 @@ const username = userField?.value?.trim() || ''; const password = pwField.value; - if (username && password) { - setTimeout(() => showSaveBanner(username, password), 500); + if (!username || !password) return; + + // Run duplicate check before showing the banner. + const credentialState = await classifyCredentials(username, password); + console.log('[PassKeeper] Credential state for', location.hostname, '→', credentialState); + + if (credentialState === 'same') { + // Credentials unchanged — silently skip. + return; } + + setTimeout(() => showSaveBanner(username, password, credentialState), 500); }, true); } diff --git a/extension/popup/popup.css b/extension/popup/popup.css index 763c979..f99c100 100644 --- a/extension/popup/popup.css +++ b/extension/popup/popup.css @@ -243,10 +243,10 @@ body { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; - padding: 8px 6px; + padding: 8px 4px; background: none; border: none; cursor: pointer; color: #9ca3af; - font-size: 10px; font-weight: 500; + font-size: 9px; font-weight: 500; transition: color 0.15s; } .nav-btn:hover { color: #374151; } diff --git a/extension/popup/popup.html b/extension/popup/popup.html index 6e9dc18..9fd20f0 100644 --- a/extension/popup/popup.html +++ b/extension/popup/popup.html @@ -126,12 +126,28 @@ Vault -