151 lines
4.8 KiB
JavaScript
151 lines
4.8 KiB
JavaScript
/**
|
|
* 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;
|
|
});
|