// Sites whose sign-in survives "Clear cookies on quit". // // The vault knows which sites have a saved login, but it is locked by the // time Theseus quits (and often for the whole session). So main keeps a copy // of just the hostnames here, refreshed whenever the vault is open, sealed // with Electron safeStorage (DPAPI / Keychain / libsecret) so it is not a // plain-text list of the user's accounts on disk. Without a real OS keystore // nothing is stored and every cookie is cleared as before. // // keepOrigins() turns the list into the origins Session.clearData() should // leave alone. Chromium matches cookies at the registrable-domain level, so // one origin per host covers the site's cookies; storage (localStorage, // IndexedDB) is per origin, so the bare and www. forms are both listed. // // File: { v: 1, data: } "use strict"; const fs = require("node:fs"); const HOST_RE = /^(?=.{1,253}$)[a-z0-9-]+(\.[a-z0-9-]+)*$/; function normHost(h) { const s = String(h || "").trim().toLowerCase().replace(/\.$/, ""); return HOST_RE.test(s) ? s : ""; } function keepOrigins(hosts) { const out = new Set(); for (const raw of hosts || []) { const h = normHost(raw); if (!h) continue; // No www. twin for an IP address or a one-label host: Chromium rejects // "www.127.0.0.1" as an origin, and one bad origin fails the whole call. const plain = /^\d+(\.\d+){3}$/.test(h) || !h.includes("."); for (const host of plain ? [h] : h.startsWith("www.") ? [h, h.slice(4)] : [h, "www." + h]) { out.add("https://" + host); out.add("http://" + host); } } return [...out]; } function createSigninSites({ file, safeStorage, log = () => {} }) { const sealOk = () => { try { if (!safeStorage || !safeStorage.isEncryptionAvailable()) return false; if (process.platform === "linux") { const b = typeof safeStorage.getSelectedStorageBackend === "function" ? safeStorage.getSelectedStorageBackend() : "unknown"; if (b === "basic_text" || b === "unknown") return false; } return true; } catch { return false; } }; let cache = null; function load() { if (cache) return cache.slice(); let list = []; try { const rec = JSON.parse(fs.readFileSync(file, "utf8")); if (rec && rec.v === 1 && rec.data && sealOk()) list = JSON.parse(safeStorage.decryptString(Buffer.from(rec.data, "base64"))); } catch { list = []; } cache = Array.isArray(list) ? list.map(normHost).filter(Boolean) : []; return cache.slice(); } function save(hosts) { const list = [...new Set((hosts || []).map(normHost).filter(Boolean))].sort(); cache = list; if (!sealOk()) { try { fs.unlinkSync(file); } catch {} return false; } try { const tmp = file + ".tmp"; fs.writeFileSync(tmp, JSON.stringify({ v: 1, data: safeStorage.encryptString(JSON.stringify(list)).toString("base64") }), { mode: 0o600 }); fs.renameSync(tmp, file); return true; } catch (e) { log("signin-sites: save failed:", e?.message || e); return false; } } return { load, save, keepOrigins: () => keepOrigins(load()) }; } module.exports = { createSigninSites, keepOrigins, normHost };