CI / Python lint (flake8) (push) Has been cancelled
CI / Python syntax check (push) Has been cancelled
CI / Alembic migration chain (push) Has been cancelled
CI / JavaScript syntax check (push) Has been cancelled
CI / Build extension zip (push) Has been cancelled
237 lines
9.4 KiB
JavaScript
237 lines
9.4 KiB
JavaScript
/**
|
|
* extension/background.firefox.js — Firefox MV2 background page.
|
|
*
|
|
* Differences from Chrome MV3 background.js:
|
|
* - Uses `browser` namespace (WebExtensions API) via the polyfill shim
|
|
* - No chrome.storage.session — session data stored in memory (object)
|
|
* and backed by storage.local with a "__session__" prefix
|
|
* - No chrome.idle API in Firefox MV2 without permission; idle lock
|
|
* is handled via a timeout-based approach instead
|
|
* - chrome.action → browser.browserAction (MV2)
|
|
*
|
|
* All message types and storage keys are identical to background.js
|
|
* so popup.js and content.js work without modification.
|
|
*/
|
|
|
|
// Firefox uses `browser` namespace; wrap with `chrome` alias if needed.
|
|
if (typeof chrome === 'undefined') {
|
|
// eslint-disable-next-line no-global-assign
|
|
chrome = browser;
|
|
}
|
|
|
|
// ── In-memory session storage shim ───────────────────────────────────────────
|
|
// Firefox MV2 does not have chrome.storage.session. We emulate it with an
|
|
// in-memory object. Data is lost when the background page is unloaded, which
|
|
// mimics the "cleared on browser close" guarantee of chrome.storage.session.
|
|
|
|
const _session = {};
|
|
|
|
const sessionStorage = {
|
|
async get(keys) {
|
|
if (typeof keys === 'string') keys = [keys];
|
|
const result = {};
|
|
for (const k of keys) result[k] = _session[k];
|
|
return result;
|
|
},
|
|
async set(obj) {
|
|
Object.assign(_session, obj);
|
|
},
|
|
async remove(keys) {
|
|
if (typeof keys === 'string') keys = [keys];
|
|
for (const k of keys) delete _session[k];
|
|
},
|
|
async clear() {
|
|
for (const k of Object.keys(_session)) delete _session[k];
|
|
},
|
|
};
|
|
|
|
// Override chrome.storage.session with our shim so the rest of the code
|
|
// works without modification.
|
|
if (!chrome.storage.session) {
|
|
chrome.storage.session = sessionStorage;
|
|
}
|
|
|
|
// ── Idle lock (timeout-based, no chrome.idle) ─────────────────────────────────
|
|
|
|
const DEFAULT_IDLE_LOCK_SECONDS = 0;
|
|
const IDLE_TIMEOUT_KEY = 'idle_lock_seconds';
|
|
let _idleTimer = null;
|
|
|
|
async function resetIdleTimer() {
|
|
const stored = await chrome.storage.local.get(IDLE_TIMEOUT_KEY);
|
|
const seconds = stored[IDLE_TIMEOUT_KEY] ?? DEFAULT_IDLE_LOCK_SECONDS;
|
|
if (_idleTimer) clearTimeout(_idleTimer);
|
|
if (seconds === 0) return; // "Never"
|
|
_idleTimer = setTimeout(async () => {
|
|
console.log('[PassKeeper] Idle lock triggered after', seconds, 's');
|
|
await chrome.storage.session.clear();
|
|
await chrome.storage.local.remove('vault_items_cs');
|
|
await chrome.storage.local.remove(HEALTH_STATUS_KEY);
|
|
const tabs = await chrome.tabs.query({});
|
|
tabs.forEach((tab) => {
|
|
if (tab.id) chrome.browserAction.setBadgeText({ text: '', tabId: tab.id });
|
|
});
|
|
}, seconds * 1000);
|
|
}
|
|
|
|
resetIdleTimer();
|
|
|
|
// ── Badge helpers ─────────────────────────────────────────────────────────────
|
|
|
|
const HEALTH_STATUS_KEY = 'health_status';
|
|
|
|
async function applyHealthBadge() {
|
|
try {
|
|
const stored = await chrome.storage.local.get(HEALTH_STATUS_KEY);
|
|
const health = stored[HEALTH_STATUS_KEY] || { breached: 0, weak: 0, reused: 0 };
|
|
const hasIssues = (health.breached || 0) + (health.weak || 0) + (health.reused || 0) > 0;
|
|
const tabs = await chrome.tabs.query({});
|
|
for (const tab of tabs) {
|
|
if (!tab.url?.startsWith('http')) continue;
|
|
const current = await new Promise((res) =>
|
|
chrome.browserAction.getBadgeText({ tabId: tab.id }, res)
|
|
).catch(() => '');
|
|
if (current && current !== '' && current !== '⚠') continue;
|
|
if (hasIssues) {
|
|
const color = (health.breached || 0) > 0 ? '#c0392b' : '#d97706';
|
|
chrome.browserAction.setBadgeText({ text: '⚠', tabId: tab.id });
|
|
chrome.browserAction.setBadgeBackgroundColor({ color, tabId: tab.id });
|
|
} else {
|
|
chrome.browserAction.setBadgeText({ text: '', tabId: tab.id });
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn('[PassKeeper] applyHealthBadge error:', err);
|
|
}
|
|
}
|
|
|
|
async function updateBadgeForTab(tabId, url) {
|
|
try {
|
|
const { vault_items } = await chrome.storage.session.get('vault_items');
|
|
if (!vault_items?.length) {
|
|
chrome.browserAction.setBadgeText({ text: '', tabId });
|
|
return;
|
|
}
|
|
let hostname;
|
|
try { hostname = new URL(url).hostname.replace(/^www\./, ''); } catch {
|
|
chrome.browserAction.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.browserAction.setBadgeText({ text: String(matches.length), tabId });
|
|
chrome.browserAction.setBadgeBackgroundColor({ color: '#1a73e8', tabId });
|
|
} else {
|
|
const stored = await chrome.storage.local.get(HEALTH_STATUS_KEY);
|
|
const health = stored[HEALTH_STATUS_KEY] || {};
|
|
const hasIssues = (health.breached || 0) + (health.weak || 0) + (health.reused || 0) > 0;
|
|
if (hasIssues) {
|
|
const color = (health.breached || 0) > 0 ? '#c0392b' : '#d97706';
|
|
chrome.browserAction.setBadgeText({ text: '⚠', tabId });
|
|
chrome.browserAction.setBadgeBackgroundColor({ color, tabId });
|
|
} else {
|
|
chrome.browserAction.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 }) => {
|
|
resetIdleTimer();
|
|
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) => {
|
|
resetIdleTimer();
|
|
|
|
if (msg.type === 'FORMS_DETECTED') {
|
|
sendResponse({ ok: true }); return false;
|
|
}
|
|
if (msg.type === 'SET_IDLE_TIMEOUT') {
|
|
resetIdleTimer(); sendResponse({ ok: true }); return false;
|
|
}
|
|
if (msg.type === 'OPEN_VAULT') {
|
|
chrome.tabs.create({ url: 'https://pwkeeper.ngodanguyen.tech/vault' });
|
|
sendResponse({ ok: true }); return false;
|
|
}
|
|
if (msg.type === 'OPEN_GENERATOR') {
|
|
chrome.storage.session.set({ popup_nav: 'generator' });
|
|
sendResponse({ ok: true }); return false;
|
|
}
|
|
if (msg.type === 'KEEPALIVE') {
|
|
sendResponse({ ok: true }); return false;
|
|
}
|
|
if (msg.type === 'VAULT_UPDATED') {
|
|
refreshAllBadges();
|
|
const payload = { type: 'VAULT_UPDATED', vault_items: msg.vault_items || [] };
|
|
chrome.tabs.query({}, (tabs) => {
|
|
tabs.forEach((tab) => {
|
|
if (tab.url?.startsWith('http://') || tab.url?.startsWith('https://')) {
|
|
chrome.tabs.sendMessage(tab.id, payload).catch(() => {});
|
|
}
|
|
});
|
|
});
|
|
sendResponse({ ok: true }); return false;
|
|
}
|
|
if (msg.type === 'SAVE_CREDENTIALS') {
|
|
chrome.storage.local.set({ pending_save: msg.data });
|
|
chrome.browserAction.setBadgeText({ text: '!' });
|
|
chrome.browserAction.setBadgeBackgroundColor({ color: '#c0392b' });
|
|
sendResponse({ ok: true }); return false;
|
|
}
|
|
if (msg.type === 'HEALTH_UPDATE') {
|
|
const { breached = 0, weak = 0, reused = 0 } = msg;
|
|
chrome.storage.local.set({ [HEALTH_STATUS_KEY]: { breached, weak, reused } });
|
|
applyHealthBadge();
|
|
sendResponse({ ok: true }); return false;
|
|
}
|
|
if (msg.type === 'CLEAR_SAVE_BADGE') {
|
|
chrome.browserAction.setBadgeText({ text: '' });
|
|
applyHealthBadge();
|
|
sendResponse({ ok: true }); return false;
|
|
}
|
|
if (msg.type === 'WEB_SESSION_SYNC') {
|
|
chrome.storage.session.set({ access_token: msg.access_token, enc_key_salt: msg.enc_key_salt || '' });
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
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;
|
|
}); |