04/20/2026 enhance the extension

This commit is contained in:
2026-04-20 16:45:09 -04:00
parent bb000e0aff
commit df8628e71c
2 changed files with 96 additions and 90 deletions
+64 -68
View File
@@ -3,58 +3,69 @@
* *
* Responsibilities: * Responsibilities:
* - Update the action badge (number of matching vault items) for the active tab. * - Update the action badge (number of matching vault items) for the active tab.
* - Bridge SAVE_CREDENTIALS messages from content script → chrome.storage.session * - Bridge SAVE_CREDENTIALS messages from content script → chrome.storage.local.
* so the popup can pick them up as a save-prompt.
* - Re-update badges when the vault cache changes. * - Re-update badges when the vault cache changes.
* - Lock the vault automatically after IDLE_LOCK_SECONDS of system inactivity.
*/ */
// ── Idle lock ─────────────────────────────────────────────────────────────────
// Lock after 10 minutes of system idle or when the screen is locked.
const IDLE_LOCK_SECONDS = 600;
chrome.idle.setDetectionInterval(IDLE_LOCK_SECONDS);
chrome.idle.onStateChanged.addListener(async (newState) => {
if (newState === 'idle' || newState === 'locked') {
console.log('[PassKeeper] System', newState, '— locking vault.');
// Clear the session (vault key, access token, vault items) so the popup
// requires master password re-entry on next open.
await chrome.storage.session.clear();
// Clear the content-script vault cache so suggestions stop showing.
await chrome.storage.local.remove('vault_items_cs');
// Clear all badge text — vault is now locked.
const tabs = await chrome.tabs.query({});
tabs.forEach(tab => {
if (tab.id) chrome.action.setBadgeText({ text: '', tabId: tab.id });
});
}
});
// ── Badge helpers ──────────────────────────────────────────────────────────── // ── Badge helpers ────────────────────────────────────────────────────────────
async function updateBadgeForTab(tabId, url) { async function updateBadgeForTab(tabId, url) {
try { try {
const { vault_items } = await chrome.storage.session.get("vault_items"); const { vault_items } = await chrome.storage.session.get('vault_items');
if (!vault_items?.length) { if (!vault_items?.length) {
chrome.action.setBadgeText({ text: "", tabId }); chrome.action.setBadgeText({ text: '', tabId });
return; return;
} }
let hostname; let hostname;
try { try { hostname = new URL(url).hostname.replace(/^www\./, ''); }
hostname = new URL(url).hostname.replace(/^www\./, ""); catch { chrome.action.setBadgeText({ text: '', tabId }); return; }
} catch {
chrome.action.setBadgeText({ text: "", tabId });
return;
}
const matches = vault_items.filter((item) => { const matches = vault_items.filter(item => {
if (item.item_type !== "password" || !item.plain?.url) return false; if (item.item_type !== 'password' || !item.plain?.url) return false;
try { try {
const h = new URL(item.plain.url).hostname.replace(/^www\./, ""); const h = new URL(item.plain.url).hostname.replace(/^www\./, '');
return ( return h === hostname || h.endsWith(`.${hostname}`) || hostname.endsWith(`.${h}`);
h === hostname || } catch { return false; }
h.endsWith(`.${hostname}`) ||
hostname.endsWith(`.${h}`)
);
} catch {
return false;
}
}); });
if (matches.length > 0) { if (matches.length > 0) {
chrome.action.setBadgeText({ text: String(matches.length), tabId }); chrome.action.setBadgeText({ text: String(matches.length), tabId });
chrome.action.setBadgeBackgroundColor({ color: "#1a73e8", tabId }); chrome.action.setBadgeBackgroundColor({ color: '#1a73e8', tabId });
} else { } else {
chrome.action.setBadgeText({ text: "", tabId }); chrome.action.setBadgeText({ text: '', tabId });
}
} catch {
/* tab may have closed */
} }
} catch { /* tab may have closed */ }
} }
async function refreshAllBadges() { async function refreshAllBadges() {
const tabs = await chrome.tabs.query({}); const tabs = await chrome.tabs.query({});
for (const tab of tabs) { for (const tab of tabs) {
if (tab.url?.startsWith("http")) updateBadgeForTab(tab.id, tab.url); if (tab.url?.startsWith('http')) updateBadgeForTab(tab.id, tab.url);
} }
} }
@@ -63,12 +74,12 @@ async function refreshAllBadges() {
chrome.tabs.onActivated.addListener(async ({ tabId }) => { chrome.tabs.onActivated.addListener(async ({ tabId }) => {
try { try {
const tab = await chrome.tabs.get(tabId); const tab = await chrome.tabs.get(tabId);
if (tab.url?.startsWith("http")) updateBadgeForTab(tabId, tab.url); if (tab.url?.startsWith('http')) updateBadgeForTab(tabId, tab.url);
} catch {} } catch { }
}); });
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === "complete" && tab.url?.startsWith("http")) { if (changeInfo.status === 'complete' && tab.url?.startsWith('http')) {
updateBadgeForTab(tabId, tab.url); updateBadgeForTab(tabId, tab.url);
} }
}); });
@@ -76,16 +87,17 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// ── Message handler ────────────────────────────────────────────────────────── // ── Message handler ──────────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// Content script requests opening the vault tab (from suggestion dropdown). // Content script requests opening the vault tab (from suggestion dropdown).
if (msg.type === "OPEN_VAULT") { if (msg.type === 'OPEN_VAULT') {
chrome.tabs.create({ url: "https://pwkeeper.ngodanguyen.tech/vault" }); chrome.tabs.create({ url: 'https://pwkeeper.ngodanguyen.tech/vault' });
sendResponse({ ok: true }); sendResponse({ ok: true });
return false; return false;
} }
// Content script requests navigating the popup to the generator view. // Content script requests navigating the popup to the generator view.
if (msg.type === "OPEN_GENERATOR") { if (msg.type === 'OPEN_GENERATOR') {
chrome.storage.session.set({ popup_nav: "generator" }); chrome.storage.session.set({ popup_nav: 'generator' });
chrome.action.openPopup().catch(() => { chrome.action.openPopup().catch(() => {
// openPopup() requires user gesture in some Chrome versions — fallback is a no-op. // openPopup() requires user gesture in some Chrome versions — fallback is a no-op.
}); });
@@ -95,26 +107,20 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
// No-op ping from popup — keeps the service worker alive while the browser // 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. // is open so chrome.storage.session is not wiped between popup openings.
if (msg.type === "KEEPALIVE") { if (msg.type === 'KEEPALIVE') {
sendResponse({ ok: true }); sendResponse({ ok: true });
return false; return false;
} }
// Popup signals vault cache was refreshed → re-check badges AND forward // Popup signals vault cache was refreshed → re-check badges AND forward
// the decrypted items directly to all content scripts (avoids storage read). // the decrypted items directly to all content scripts (avoids storage read).
if (msg.type === "VAULT_UPDATED") { if (msg.type === 'VAULT_UPDATED') {
refreshAllBadges(); refreshAllBadges();
const payload = { const payload = { type: 'VAULT_UPDATED', vault_items: msg.vault_items || [] };
type: "VAULT_UPDATED",
vault_items: msg.vault_items || [],
};
chrome.tabs.query({}, function (tabs) { chrome.tabs.query({}, function (tabs) {
tabs.forEach(function (tab) { tabs.forEach(function (tab) {
if ( if (tab.url && (tab.url.startsWith('http://') || tab.url.startsWith('https://'))) {
tab.url && chrome.tabs.sendMessage(tab.id, payload).catch(function () { });
(tab.url.startsWith("http://") || tab.url.startsWith("https://"))
) {
chrome.tabs.sendMessage(tab.id, payload).catch(function () {});
} }
}); });
}); });
@@ -123,7 +129,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
} }
// Content script detected credentials on form submit → store for save-prompt. // Content script detected credentials on form submit → store for save-prompt.
if (msg.type === "SAVE_CREDENTIALS") { if (msg.type === 'SAVE_CREDENTIALS') {
// Store in local storage so the prompt survives service worker restarts // Store in local storage so the prompt survives service worker restarts
// and is guaranteed to be present when the user next opens the popup. // and is guaranteed to be present when the user next opens the popup.
chrome.storage.local.set({ pending_save: msg.data }); chrome.storage.local.set({ pending_save: msg.data });
@@ -132,10 +138,10 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
} }
// Bridge: web app logged in → store tokens in extension storage. // Bridge: web app logged in → store tokens in extension storage.
if (msg.type === "WEB_SESSION_SYNC") { if (msg.type === 'WEB_SESSION_SYNC') {
chrome.storage.session.set({ chrome.storage.session.set({
access_token: msg.access_token, access_token: msg.access_token,
enc_key_salt: msg.enc_key_salt || "", enc_key_salt: msg.enc_key_salt || '',
}); });
// Also persist enc_key_salt locally so the unlock-only view survives browser restarts // Also persist enc_key_salt locally so the unlock-only view survives browser restarts
chrome.storage.local.set({ chrome.storage.local.set({
@@ -147,35 +153,25 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
} }
// Bridge: web app logged out → clear extension session. // Bridge: web app logged out → clear extension session.
if (msg.type === "WEB_SESSION_CLEAR") { if (msg.type === 'WEB_SESSION_CLEAR') {
chrome.storage.session.remove([ chrome.storage.session.remove(['access_token', 'vault_key_jwk', 'vault_items', 'enc_key_salt']);
"access_token", chrome.storage.local.remove('refresh_token');
"vault_key_jwk",
"vault_items",
"enc_key_salt",
]);
chrome.storage.local.remove("refresh_token");
sendResponse({ ok: true }); sendResponse({ ok: true });
return false; return false;
} }
// Popup logged in via extension → inject session into open vault tabs. // Popup logged in via extension → inject session into open vault tabs.
if (msg.type === "EXT_SESSION_SYNC") { if (msg.type === 'EXT_SESSION_SYNC') {
chrome.tabs.query( chrome.tabs.query({ url: 'https://pwkeeper.ngodanguyen.tech/*' }, tabs => {
{ url: "https://pwkeeper.ngodanguyen.tech/*" }, tabs.forEach(tab => {
(tabs) => { chrome.tabs.sendMessage(tab.id, {
tabs.forEach((tab) => { type: 'INJECT_SESSION',
chrome.tabs
.sendMessage(tab.id, {
type: "INJECT_SESSION",
access_token: msg.access_token, access_token: msg.access_token,
refresh_token: msg.refresh_token, refresh_token: msg.refresh_token,
enc_key_salt: msg.enc_key_salt, enc_key_salt: msg.enc_key_salt,
}) }).catch(() => { });
.catch(() => {}); });
}); });
},
);
sendResponse({ ok: true }); sendResponse({ ok: true });
return false; return false;
} }
+22 -12
View File
@@ -3,16 +3,19 @@
"name": "PassKeeper", "name": "PassKeeper",
"version": "1.0.0", "version": "1.0.0",
"description": "Autofill and manage your PassKeeper vault", "description": "Autofill and manage your PassKeeper vault",
"permissions": [
"permissions": ["storage", "activeTab", "tabs"], "storage",
"host_permissions": [ "activeTab",
"https://pwkeeper.ngodanguyen.tech/*" "tabs",
"idle"
],
"host_permissions": [
"http://*/*",
"https://*/*"
], ],
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
}, },
"action": { "action": {
"default_popup": "popup/popup.html", "default_popup": "popup/popup.html",
"default_title": "PassKeeper", "default_title": "PassKeeper",
@@ -22,20 +25,27 @@
"128": "icons/icon128.png" "128": "icons/icon128.png"
} }
}, },
"content_scripts": [ "content_scripts": [
{ {
"matches": ["http://*/*", "https://*/*"], "matches": [
"js": ["content/content.js"], "http://*/*",
"https://*/*"
],
"js": [
"content/content.js"
],
"run_at": "document_idle" "run_at": "document_idle"
}, },
{ {
"matches": ["https://pwkeeper.ngodanguyen.tech/*"], "matches": [
"js": ["bridge/bridge.js"], "https://pwkeeper.ngodanguyen.tech/*"
],
"js": [
"bridge/bridge.js"
],
"run_at": "document_idle" "run_at": "document_idle"
} }
], ],
"icons": { "icons": {
"16": "icons/icon16.png", "16": "icons/icon16.png",
"48": "icons/icon48.png", "48": "icons/icon48.png",