04/26 New features, new for Firefox
This commit is contained in:
+99
-3
@@ -9,6 +9,41 @@ const Vault = (() => {
|
||||
let _items = [];
|
||||
let _folders = [];
|
||||
let _currentView = "vault";
|
||||
|
||||
// ── Web-app inactivity / session timeout ──────────────────────────────────
|
||||
// Mirrors the extension idle lock. Default 15 min; user can change in
|
||||
// Account Settings. Zero means never. Stored in localStorage so it persists
|
||||
// across page reloads without a server round-trip.
|
||||
|
||||
const WEB_IDLE_KEY = 'web_idle_minutes';
|
||||
const WEB_IDLE_DEFAULT = 15; // minutes; 0 = never
|
||||
let _webIdleTimer = null;
|
||||
|
||||
function _getWebIdleMinutes() {
|
||||
const v = parseInt(localStorage.getItem(WEB_IDLE_KEY), 10);
|
||||
return isNaN(v) ? WEB_IDLE_DEFAULT : v;
|
||||
}
|
||||
|
||||
function _resetWebIdleTimer() {
|
||||
if (_webIdleTimer) clearTimeout(_webIdleTimer);
|
||||
const mins = _getWebIdleMinutes();
|
||||
if (mins === 0) return;
|
||||
_webIdleTimer = setTimeout(() => {
|
||||
// Only lock if the vault is currently unlocked.
|
||||
if (!VaultSession.getKey()) return;
|
||||
console.log('[PassKeeper] Web inactivity lock after', mins, 'min');
|
||||
VaultSession.clear();
|
||||
showUnlockOverlay();
|
||||
showToast('Vault locked due to inactivity.', 'info');
|
||||
}, mins * 60_000);
|
||||
}
|
||||
|
||||
function _startWebIdleTracking() {
|
||||
const events = ['mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll'];
|
||||
const handler = () => _resetWebIdleTimer();
|
||||
events.forEach((e) => document.addEventListener(e, handler, { passive: true }));
|
||||
_resetWebIdleTimer();
|
||||
}
|
||||
let _activeFilter = null;
|
||||
let _sortOrder = "name-asc";
|
||||
|
||||
@@ -197,7 +232,7 @@ const Vault = (() => {
|
||||
|
||||
// ── View switching ────────────────────────────────────────────────────────
|
||||
|
||||
function switchView(view) {
|
||||
function switchView(view, { pushState = true } = {}) {
|
||||
_currentView = view;
|
||||
["vault", "security", "sharing", "emergency", "import-export"].forEach((v) => {
|
||||
document
|
||||
@@ -215,6 +250,12 @@ const Vault = (() => {
|
||||
.forEach((el) => el.classList.remove("active"));
|
||||
}
|
||||
|
||||
// Push to browser history so the back button navigates between views.
|
||||
if (pushState) {
|
||||
const hash = view === "vault" ? "" : `#${view}`;
|
||||
history.pushState({ view }, "", hash || window.location.pathname);
|
||||
}
|
||||
|
||||
if (view === "security") renderSecurityDashboard();
|
||||
if (view === "sharing") loadSharingView();
|
||||
if (view === "emergency") loadEmergencyView();
|
||||
@@ -643,6 +684,19 @@ const Vault = (() => {
|
||||
);
|
||||
makeSection("Old Passwords", "🕐", old, "Weak or reused passwords not changed in over 180 days.");
|
||||
|
||||
// ── Missing 2FA warning ──────────────────────────────────────────────────
|
||||
// Flag password items that have a URL but no TOTP URI saved.
|
||||
// These accounts likely support 2FA but the user hasn't stored it.
|
||||
const noTotp = pwItems.filter((i) =>
|
||||
i.plain?.url && !extractTotpSecret(i.plain?.totp_uri)
|
||||
);
|
||||
makeSection(
|
||||
'No 2FA Saved',
|
||||
'🔓',
|
||||
noTotp,
|
||||
'These accounts may support two-factor authentication but have no TOTP code stored.',
|
||||
);
|
||||
|
||||
// ── HaveIBeenPwned breach check ──────────────────────────────────────────
|
||||
// Run after the synchronous sections are rendered so the UI is immediately
|
||||
// useful. The HIBP API is queried in parallel for all passwords.
|
||||
@@ -1798,6 +1852,9 @@ const Vault = (() => {
|
||||
|
||||
async function openSettingsModal() {
|
||||
document.getElementById("settings-modal").classList.add("open");
|
||||
// Populate web-app session timeout select with saved value.
|
||||
const idleSel = document.getElementById("web-idle-select");
|
||||
if (idleSel) idleSel.value = String(_getWebIdleMinutes());
|
||||
// Reset change password fields
|
||||
["cp-current", "cp-new", "cp-confirm"].forEach((id) => {
|
||||
const el = document.getElementById(id);
|
||||
@@ -2717,10 +2774,21 @@ const Vault = (() => {
|
||||
return 30 - (Math.floor(Date.now() / 1000) % 30);
|
||||
}
|
||||
|
||||
// Track the clipboard clear timer so multiple rapid copies don't stack.
|
||||
let _clipboardClearTimer = null;
|
||||
|
||||
function copyToClipboard(text, msg) {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => showToast(msg))
|
||||
.then(() => {
|
||||
showToast(msg);
|
||||
// Auto-clear clipboard after 30 seconds — industry-standard hygiene.
|
||||
if (_clipboardClearTimer) clearTimeout(_clipboardClearTimer);
|
||||
_clipboardClearTimer = setTimeout(() => {
|
||||
navigator.clipboard.writeText('').catch(() => {});
|
||||
_clipboardClearTimer = null;
|
||||
}, 30_000);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
@@ -2979,6 +3047,16 @@ const Vault = (() => {
|
||||
document
|
||||
.getElementById("btn-close-settings")
|
||||
?.addEventListener("click", () => closeModal("settings-modal"));
|
||||
|
||||
// Web-app session timeout setting.
|
||||
document.getElementById("web-idle-select")?.addEventListener("change", (e) => {
|
||||
const mins = parseInt(e.target.value, 10);
|
||||
localStorage.setItem(WEB_IDLE_KEY, mins);
|
||||
_resetWebIdleTimer();
|
||||
const label = mins === 0 ? 'never' : mins + ' min';
|
||||
showToast('Auto-lock set to ' + label + '.');
|
||||
console.log('[PassKeeper] Web idle timeout set to', label);
|
||||
});
|
||||
document
|
||||
.getElementById("settings-modal")
|
||||
?.addEventListener("click", (e) => {
|
||||
@@ -3161,10 +3239,28 @@ const Vault = (() => {
|
||||
document.getElementById("btn-add-item")?.click();
|
||||
});
|
||||
|
||||
// ── Browser history / back-button support ─────────────────────────────────
|
||||
const _hashToView = (hash) => {
|
||||
const v = (hash || '').replace('#', '').trim();
|
||||
const valid = ['vault', 'security', 'sharing', 'emergency', 'import-export'];
|
||||
return valid.includes(v) ? v : 'vault';
|
||||
};
|
||||
const _initialView = _hashToView(window.location.hash);
|
||||
history.replaceState({ view: _initialView }, '', window.location.href);
|
||||
window.addEventListener('popstate', (e) => {
|
||||
const view = e.state?.view || _hashToView(window.location.hash);
|
||||
switchView(view, { pushState: false });
|
||||
});
|
||||
|
||||
// Start web-app inactivity tracking.
|
||||
_startWebIdleTracking();
|
||||
|
||||
if (!VaultSession.getKey()) {
|
||||
showUnlockOverlay();
|
||||
} else {
|
||||
loadVault();
|
||||
// Restore view from hash after vault loads (avoids rendering before decrypt).
|
||||
if (_initialView !== 'vault') switchView(_initialView, { pushState: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3268,4 +3364,4 @@ const Vault = (() => {
|
||||
}
|
||||
})();
|
||||
|
||||
document.addEventListener("DOMContentLoaded", Vault.init);
|
||||
document.addEventListener("DOMContentLoaded", Vault.init);
|
||||
|
||||
@@ -853,6 +853,24 @@
|
||||
</div>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<div class="settings-section">
|
||||
<h4 class="settings-section-title">⏱️ Auto-Lock</h4>
|
||||
<p class="settings-desc">
|
||||
Automatically lock the vault after a period of inactivity.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="web-idle-select">Lock after</label>
|
||||
<select id="web-idle-select" class="form-control">
|
||||
<option value="0">Never</option>
|
||||
<option value="5">5 minutes</option>
|
||||
<option value="10">10 minutes</option>
|
||||
<option value="15" selected>15 minutes</option>
|
||||
<option value="30">30 minutes</option>
|
||||
<option value="60">1 hour</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section settings-section-danger">
|
||||
<h4 class="settings-section-title">⚠️ Danger Zone</h4>
|
||||
<p class="settings-desc">
|
||||
@@ -1005,4 +1023,4 @@
|
||||
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/sharing.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/vault.js') }}"></script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 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 = 600;
|
||||
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');
|
||||
const tabs = await chrome.tabs.query({});
|
||||
tabs.forEach((tab) => {
|
||||
if (tab.id) chrome.browserAction.setBadgeText({ text: '', tabId: tab.id });
|
||||
});
|
||||
}, seconds * 1000);
|
||||
}
|
||||
|
||||
resetIdleTimer();
|
||||
|
||||
// ── Badge helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
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 {
|
||||
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 === 'CLEAR_SAVE_BADGE') {
|
||||
chrome.browserAction.setBadgeText({ text: '' });
|
||||
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;
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "PassKeeper",
|
||||
"version": "1.0.0",
|
||||
"description": "Autofill and manage your PassKeeper vault",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"activeTab",
|
||||
"tabs",
|
||||
"idle",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"browser_action": {
|
||||
"default_popup": "popup/popup.html",
|
||||
"default_title": "PassKeeper",
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
"scripts": ["background.firefox.js"],
|
||||
"persistent": false
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["http://*/*", "https://*/*"],
|
||||
"js": ["shared/browser-polyfill.js", "content/content.js"],
|
||||
"run_at": "document_idle"
|
||||
},
|
||||
{
|
||||
"matches": ["https://pwkeeper.ngodanguyen.tech/*"],
|
||||
"js": ["shared/browser-polyfill.js", "bridge/bridge.js"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"commands": {
|
||||
"_execute_browser_action": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+L",
|
||||
"mac": "Command+Shift+L"
|
||||
},
|
||||
"description": "Open PassKeeper"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,17 @@ function escHtml(str) {
|
||||
.replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// Auto-clear clipboard 30 s after a sensitive copy.
|
||||
let _clipTimer = null;
|
||||
function _copyWithAutoClear(text) {
|
||||
navigator.clipboard.writeText(text).catch(() => {});
|
||||
if (_clipTimer) clearTimeout(_clipTimer);
|
||||
_clipTimer = setTimeout(() => {
|
||||
navigator.clipboard.writeText('').catch(() => {});
|
||||
_clipTimer = null;
|
||||
}, 30_000);
|
||||
}
|
||||
|
||||
// ── Avatar helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
const AVATAR_COLORS = ['', 'color-red', 'color-green', 'color-purple', 'color-teal'];
|
||||
@@ -539,7 +550,7 @@ function renderList() {
|
||||
e.stopPropagation();
|
||||
const item = _items.find(i => i.id === parseInt(btn.dataset.copyPass));
|
||||
if (item?.plain?.password) {
|
||||
navigator.clipboard.writeText(item.plain.password);
|
||||
_copyWithAutoClear(item.plain.password);
|
||||
btn.title = 'Copied!';
|
||||
setTimeout(() => { btn.title = 'Copy password'; }, 1500);
|
||||
}
|
||||
@@ -586,7 +597,7 @@ function renderList() {
|
||||
e.stopPropagation();
|
||||
const item = _items.find(i => i.id === parseInt(btn.dataset.copyUser));
|
||||
if (item?.plain?.username) {
|
||||
navigator.clipboard.writeText(item.plain.username);
|
||||
_copyWithAutoClear(item.plain.username);
|
||||
btn.title = 'Copied!';
|
||||
setTimeout(() => { btn.title = 'Copy username'; }, 1500);
|
||||
}
|
||||
@@ -615,12 +626,12 @@ function renderList() {
|
||||
item.plain?.username ? {
|
||||
label: 'Copy username',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.7"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>',
|
||||
action: () => { navigator.clipboard.writeText(item.plain.username); flyout.remove(); },
|
||||
action: () => { _copyWithAutoClear(item.plain.username); flyout.remove(); },
|
||||
} : null,
|
||||
item.plain?.password ? {
|
||||
label: 'Copy password',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><rect x="3" y="10" width="18" height="11" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="M8 10V7a4 4 0 018 0v3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>',
|
||||
action: () => { navigator.clipboard.writeText(item.plain.password); flyout.remove(); },
|
||||
action: () => { _copyWithAutoClear(item.plain.password); flyout.remove(); },
|
||||
} : null,
|
||||
].filter(Boolean);
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* shared/browser-polyfill.js
|
||||
*
|
||||
* Minimal shim so content scripts and bridge.js can use `chrome.*` on Firefox.
|
||||
* Firefox exposes the WebExtensions API as `browser.*` (promise-based).
|
||||
* Chrome exposes it as `chrome.*` (callback-based, with Promise wrappers in MV3).
|
||||
*
|
||||
* This shim only aliases `browser` → `chrome` when `chrome` is not defined,
|
||||
* which is sufficient for the subset of APIs used by PassKeeper content scripts
|
||||
* (chrome.runtime, chrome.storage.session/local, chrome.storage.onChanged).
|
||||
*
|
||||
* For full cross-browser compatibility, replace with the official
|
||||
* Mozilla WebExtension browser-polyfill:
|
||||
* https://github.com/mozilla/webextension-polyfill
|
||||
*/
|
||||
if (typeof chrome === 'undefined' && typeof browser !== 'undefined') {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
chrome = browser;
|
||||
}
|
||||
Reference in New Issue
Block a user