04/20/2026 enhance the extension
This commit is contained in:
+68
-72
@@ -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 {
|
} catch { /* tab may have closed */ }
|
||||||
/* 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
|
access_token: msg.access_token,
|
||||||
.sendMessage(tab.id, {
|
refresh_token: msg.refresh_token,
|
||||||
type: "INJECT_SESSION",
|
enc_key_salt: msg.enc_key_salt,
|
||||||
access_token: msg.access_token,
|
}).catch(() => { });
|
||||||
refresh_token: msg.refresh_token,
|
});
|
||||||
enc_key_salt: msg.enc_key_salt,
|
});
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
sendResponse({ ok: true });
|
sendResponse({ ok: true });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-16
@@ -3,42 +3,52 @@
|
|||||||
"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",
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
"16": "icons/icon16.png",
|
"16": "icons/icon16.png",
|
||||||
"48": "icons/icon48.png",
|
"48": "icons/icon48.png",
|
||||||
"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",
|
||||||
"128": "icons/icon128.png"
|
"128": "icons/icon128.png"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user