04/19 Update extension: persistent session, duplication check - again

This commit is contained in:
2026-04-19 17:25:35 -04:00
parent e3d007b654
commit 66e0129994
5 changed files with 111 additions and 14 deletions
+72 -9
View File
@@ -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 = `
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><rect x="3" y="9" width="18" height="12" rx="2" stroke="#1a73e8" stroke-width="1.8"/><path d="M8 9V6a4 4 0 018 0v3" stroke="#1a73e8" stroke-width="1.8" stroke-linecap="round"/></svg>
<strong style="flex:1;font-size:13px;color:#111827;">Save to PassKeeper?</strong>
<strong style="flex:1;font-size:13px;color:#111827;">${escHtml(title)}</strong>
<button id="__pk_close__" style="background:none;border:none;cursor:pointer;font-size:18px;color:#9ca3af;line-height:1;padding:0;">×</button>
</div>
<p style="color:#6b7280;font-size:12px;margin-bottom:10px;">
@@ -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);
}