import { decryptVault, KeychainApiClient, type User, type VaultEntry, } from "@keychain/core"; import { getSettings, originFromUrl } from "./extension"; import "./extension.css"; const rootElement = document.querySelector("#popup-app"); if (!rootElement) throw new Error("Popup root is missing"); const root = rootElement; let activeTabId: number | undefined; let activeOrigin: string | null = null; let unlocked = false; let entries: VaultEntry[] = []; let busy = false; let errorMessage = ""; function escapeHtml(value: string) { return value.replace(/[&<>'"]/g, (character) => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """ })[character] || character); } function initials(title: string) { const words = title.trim().split(/\s+/).filter(Boolean); return escapeHtml((words.length > 1 ? words[0][0] + words[1][0] : title.slice(0, 2)).toUpperCase()); } function render() { const originLabel = activeOrigin ? escapeHtml(activeOrigin.replace(/^https?:\/\//, "")) : "This page"; if (!unlocked) { root.innerHTML = `
Keychain
Locked
Fill for${originLabel}
Private by design

Unlock to fill

Your master password stays in this browser and is never sent to the server.

${errorMessage ? `
${escapeHtml(errorMessage)}
` : ""}
Encrypted locally
`; document.querySelector("#unlock-extension")?.addEventListener("submit", unlock); document.querySelector("#open-options")?.addEventListener("click", () => chrome.runtime.openOptionsPage()); return; } const matches = entries.filter((entry) => originFromUrl(entry.url) === activeOrigin); root.innerHTML = `
Keychain
Matches for${originLabel}
Ready to fill

${matches.length ? `${matches.length} login${matches.length === 1 ? "" : "s"} found` : "No login found"}

${matches.length ? "Choose a login to fill. Keychain will not submit the form." : "Add a password with this website URL in your vault to use it here."}

${matches.map((entry) => ``).join("") || `
Nothing for this origin yetExact origin matching protects you from accidental fills.
`}
Unlocked in memory only
`; document.querySelectorAll("[data-entry-id]").forEach((button) => button.addEventListener("click", () => fill(button.dataset.entryId || ""))); document.querySelector("#lock-extension")?.addEventListener("click", lock); document.querySelector("#open-options")?.addEventListener("click", () => chrome.runtime.openOptionsPage()); } async function unlock(event: SubmitEvent) { event.preventDefault(); const form = event.currentTarget as HTMLFormElement; const data = new FormData(form); busy = true; errorMessage = ""; render(); try { const settings = await getSettings(); const client = new KeychainApiClient(settings.apiUrl); const user: User = await client.login(String(data.get("email")), String(data.get("accountPassword"))); const response = await client.getVault(); if (!response.envelope) throw new Error("This account has no encrypted vault yet"); const document = await decryptVault(response.envelope, String(data.get("masterPassword")), user.id); entries = document.entries; await chrome.runtime.sendMessage({ type: "SET_VAULT", entries }); unlocked = true; } catch (error) { errorMessage = error instanceof Error ? error.message : "Unable to unlock"; } finally { busy = false; render(); } } async function fill(entryId: string) { if (!activeTabId) return; const response = await chrome.runtime.sendMessage({ type: "FILL_ENTRY", entryId, tabId: activeTabId }); if (!response?.ok) { errorMessage = response?.error || "Unable to fill this login"; render(); } else { window.setTimeout(() => window.close(), 180); } } async function lock() { entries = []; unlocked = false; await chrome.runtime.sendMessage({ type: "LOCK" }); render(); } async function start() { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); activeTabId = tab?.id; activeOrigin = originFromUrl(tab?.url); const status = await chrome.runtime.sendMessage({ type: "STATUS" }); unlocked = Boolean(status?.unlocked); if (unlocked && activeOrigin) { const result = await chrome.runtime.sendMessage({ type: "MATCHING_ENTRIES", origin: activeOrigin }); entries = Array.isArray(result?.entries) ? result.entries : []; } render(); } void start();