const extPopup = globalThis.browser ?? globalThis.chrome; const usePromiseAPI = typeof globalThis.browser !== "undefined"; function runtimeSend(message) { if (usePromiseAPI) { return extPopup.runtime.sendMessage(message); } return new Promise((resolve, reject) => { extPopup.runtime.sendMessage(message, (response) => { const error = extPopup.runtime.lastError; if (error) { reject(new Error(error.message)); return; } resolve(response); }); }); } function hostFromURL(rawURL) { try { return new URL(rawURL).host || rawURL; } catch (_error) { return rawURL || "Current page"; } } function setStatus(title, message, tone) { const card = document.getElementById("status-card"); card.dataset.tone = tone || "neutral"; document.getElementById("status-title").textContent = title; document.getElementById("status-message").textContent = message; } function matchSubtitle(match) { const parts = []; if (match.username) { parts.push(match.username); } if (Array.isArray(match.path) && match.path.length !== 0) { parts.push(match.path.join(" / ")); } return parts.join(" · ") || "No username"; } function saveCardLabel(pendingSave) { return pendingSave?.mode === "update" ? `Update ${pendingSave.title || "Login"}` : "Save Login"; } function renderMatchList(root, matches, options = {}) { const targetTabID = popupTabID(); const emptyMessage = options.emptyMessage || "No matching entries."; root.textContent = ""; if (!Array.isArray(matches) || matches.length === 0) { const empty = document.createElement("p"); empty.className = "subtle"; empty.textContent = emptyMessage; root.appendChild(empty); return; } for (const match of matches) { const row = document.createElement("button"); row.type = "button"; row.className = "match-row"; const main = document.createElement("span"); main.className = "match-main"; const title = document.createElement("strong"); title.textContent = match.title; const subtitle = document.createElement("span"); subtitle.className = "subtle"; subtitle.textContent = matchSubtitle(match); const quality = document.createElement("span"); quality.className = "quality"; quality.textContent = match.quality || ""; main.appendChild(title); main.appendChild(subtitle); row.appendChild(main); row.appendChild(quality); row.addEventListener("click", async () => { row.disabled = true; try { if (typeof options.onSelect === "function") { await options.onSelect(match, targetTabID); } else { setStatus("Approval may be required", "KeePassGO will prompt if this token needs approval before fill.", "warning"); const result = await runtimeSend({ type: "keepassgo-fill-entry", entryId: match.id, tabId: targetTabID }); if (!result?.success) { throw new Error(result?.error || "Fill failed."); } setStatus("Filled", `${match.title} was sent to the current page.`, "ready"); } } catch (error) { setStatus(options.onSelect ? "Save failed" : "Fill failed", error instanceof Error ? error.message : String(error), "error"); } finally { row.disabled = false; } }); root.appendChild(row); } } function renderMatches(state) { const emptyMessage = state.pageHasLoginForm ? "No matching entries for this page." : "No login fields detected on this page."; const root = document.getElementById("matches"); if (state.pendingSave) { renderMatchList(root, state.matches, { emptyMessage, onSelect: async (match, targetTabID) => { const result = await runtimeSend({ type: "keepassgo-save-login", tabId: targetTabID, selectedMatch: { id: match.id, title: match.title, path: Array.isArray(match.path) ? match.path : [] } }); if (!result?.success) { throw new Error(result?.error || "Save failed."); } setStatus("Saved", `${state.pendingSave.title || "Login"} is now in KeePassGO.`, "ready"); document.getElementById("save-card").hidden = true; } }); return; } renderMatchList(root, state.matches, { emptyMessage }); } function renderSearchResults(results, query) { const root = document.getElementById("search-results"); if (!query) { root.textContent = ""; const hint = document.createElement("p"); hint.className = "subtle"; hint.textContent = "Search all entries you can access with this token."; root.appendChild(hint); return; } renderMatchList(root, results, { emptyMessage: `No entries matched "${query}".` }); } function renderPageHint(state) { const hint = document.getElementById("page-hint"); if (state.pendingFill) { hint.textContent = "Approval is pending in KeePassGO."; return; } if (state.pageHasLoginForm && Array.isArray(state.matches) && state.matches.length > 0) { hint.textContent = "Inline KeePassGO suggestions are available on the page."; return; } if (state.pageHasLoginForm) { hint.textContent = "KeePassGO checked this login form already."; return; } hint.textContent = "Open a sign-in page to see KeePassGO suggestions here."; } function renderPendingSave(state) { const card = document.getElementById("save-card"); const message = document.getElementById("save-message"); const action = document.getElementById("save-action"); const pendingSave = state.pendingSave; if (!pendingSave) { card.hidden = true; action.onclick = null; return; } card.hidden = false; action.textContent = saveCardLabel(pendingSave); if (pendingSave.mode === "update") { message.textContent = `KeePassGO can update ${pendingSave.title || "this login"} with the submitted password.`; } else if (Array.isArray(pendingSave.path) && pendingSave.path.length > 0) { message.textContent = `KeePassGO can save this login in ${pendingSave.path.join(" / ")}. Search the vault to choose a different group if needed.`; } else { message.textContent = "Search the vault below to choose a group for this submitted login."; } action.disabled = pendingSave.mode !== "update" && (!Array.isArray(pendingSave.path) || pendingSave.path.length === 0); action.onclick = async () => { action.disabled = true; try { const result = await runtimeSend({ type: "keepassgo-save-login", tabId: popupTabID() }); if (!result?.success) { throw new Error(result?.error || "Save failed."); } setStatus("Saved", `${pendingSave.title || "Login"} is now in KeePassGO.`, "ready"); card.hidden = true; } catch (error) { setStatus("Save failed", error instanceof Error ? error.message : String(error), "error"); } finally { action.disabled = false; } }; } function popupTabID() { const rawValue = new URLSearchParams(window.location.search).get("tabId"); if (rawValue === null) { return null; } const parsed = Number.parseInt(rawValue, 10); return Number.isInteger(parsed) ? parsed : null; } async function searchVault(event) { event.preventDefault(); const query = document.getElementById("search-query").value.trim(); const resultsRoot = document.getElementById("search-results"); if (!query) { renderSearchResults([], ""); return; } resultsRoot.textContent = ""; const loading = document.createElement("p"); loading.className = "subtle"; loading.textContent = "Searching KeePassGO…"; resultsRoot.appendChild(loading); try { const response = await runtimeSend({ type: "keepassgo-search-logins", query }); if (!response?.success) { throw new Error(response?.error || "Search failed."); } renderSearchResults(Array.isArray(response.results) ? response.results : [], query); } catch (error) { renderSearchResults([], query); setStatus("Search failed", error instanceof Error ? error.message : String(error), "error"); } } async function main() { try { document.getElementById("search-form").addEventListener("submit", searchVault); renderSearchResults([], ""); const state = await runtimeSend({ type: "keepassgo-popup-state", force: true, tabId: popupTabID() }); document.getElementById("page-host").textContent = hostFromURL(state.pageUrl || ""); renderPageHint(state); renderPendingSave(state); if (!state.configured) { setStatus("Configure access", state.error || "Set the API token in extension settings.", "warning"); renderMatches({ matches: [] }); return; } if (state.pendingFill) { setStatus("Approval needed", state.pendingMessage || "Approve or deny the fill request in KeePassGO.", "warning"); renderMatches(state); return; } if (!state.success) { setStatus("KeePassGO unavailable", state.error || "The native host could not reach KeePassGO.", "error"); renderMatches(state); return; } if (state.status?.locked) { setStatus("Vault locked", "Unlock KeePassGO, then try the page again.", "warning"); renderMatches(state); return; } const count = Array.isArray(state.matches) ? state.matches.length : 0; if (!state.pageHasLoginForm) { setStatus("Ready", "KeePassGO is connected. Open a login form to check for matches.", "ready"); } else if (state.pendingSave) { setStatus("Save submitted login", state.pendingSave.mode === "update" ? `Update ${state.pendingSave.title || "this login"} or pick a different target below.` : "Save this submitted login or search below to choose a target entry.", "ready"); } else if (count === 0) { setStatus("Checked this page", "KeePassGO did not find a matching login for this form.", "ready"); } else { setStatus("Page suggestions ready", count === 1 ? "1 matching entry is ready on this page." : `${count} matching entries are ready on this page.`, "ready"); } renderMatches(state); } catch (error) { setStatus("Error", error instanceof Error ? error.message : String(error), "error"); renderMatches({ matches: [] }); } } void main();