/** * extension/bridge/bridge.js * * Content script injected ONLY on pwkeeper.ngodanguyen.tech. * * Web app → Extension: * Listens for the 'passkeeper:session' custom event dispatched by auth.js * after login, and forwards access_token + refresh_token + enc_key_salt to * the background service worker via chrome.runtime.sendMessage. * * Extension → Web app: * Listens for INJECT_SESSION messages from the background (triggered when the * user logs in through the popup). Writes tokens into the web app's storage * and fires 'passkeeper:ext-login' so auth.js can unlock the vault overlay. */ (() => { 'use strict'; // ── Web app → Extension ──────────────────────────────────────────────────── function syncToExtension(access_token, refresh_token, enc_key_salt) { if (!access_token || !refresh_token) return; chrome.runtime.sendMessage({ type: 'WEB_SESSION_SYNC', access_token, refresh_token, enc_key_salt: enc_key_salt || '', }).catch(() => {}); } // Fired by auth.js completeLogin window.addEventListener('passkeeper:session', e => { const { access_token, refresh_token, enc_key_salt } = e.detail || {}; syncToExtension(access_token, refresh_token, enc_key_salt); }); // Fired by auth.js on logout / sign-out window.addEventListener('passkeeper:logout', () => { chrome.runtime.sendMessage({ type: 'WEB_SESSION_CLEAR' }).catch(() => {}); }); // On initial page load: sync in both directions // Web → Extension: if the web app is already logged in, forward tokens to extension const existing_token = sessionStorage.getItem('access_token'); const existing_refresh = localStorage.getItem('refresh_token'); const existing_salt = sessionStorage.getItem('enc_key_salt'); if (existing_token && existing_refresh) { syncToExtension(existing_token, existing_refresh, existing_salt); } // Extension → Web: if the web app has no session but the extension does, // refresh the access token first (so we inject a guaranteed-fresh pair) then // fire passkeeper:ext-login so vault.js can proceed without hitting 401. if (!existing_token) { (async () => { try { const local = await chrome.storage.local.get(['refresh_token', 'enc_key_salt']); const refresh = local.refresh_token; const salt = local.enc_key_salt || (await chrome.storage.session.get('enc_key_salt')).enc_key_salt || ''; if (!refresh || !salt) return; // Exchange the stored refresh token for a fresh access token const res = await fetch(`${location.origin}/api/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: refresh }), }); if (!res.ok) return; const data = await res.json(); const freshAccess = data.access_token; const freshRefresh = data.refresh_token || refresh; // Write fresh tokens into the page session so vault.js works immediately sessionStorage.setItem('access_token', freshAccess); localStorage.setItem('refresh_token', freshRefresh); sessionStorage.setItem('enc_key_salt', salt); // Keep extension storage in sync with the rotated tokens chrome.storage.session.set({ access_token: freshAccess, enc_key_salt: salt }); chrome.storage.local.set({ refresh_token: freshRefresh, enc_key_salt: salt }); window.dispatchEvent(new CustomEvent('passkeeper:ext-login', { detail: { enc_key_salt: salt }, })); } catch {} })(); } // ── Extension → Web app ──────────────────────────────────────────────────── chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { if (msg.type === 'INJECT_SESSION') { // Write tokens so the web app is effectively logged in sessionStorage.setItem('access_token', msg.access_token); localStorage.setItem('refresh_token', msg.refresh_token); if (msg.enc_key_salt) sessionStorage.setItem('enc_key_salt', msg.enc_key_salt); // Tell the web app a session was injected — vault.js listens for this // to dismiss the unlock overlay and load the vault window.dispatchEvent(new CustomEvent('passkeeper:ext-login', { detail: { enc_key_salt: msg.enc_key_salt }, })); sendResponse({ ok: true }); } return false; }); })();