// Theseus Navigator β€” Electron main process. // Native .bch via a custom `bns://` protocol: resolves names with the shared // portable resolver (Argus/resolver-web.js) and serves content itself (on-chain // h, Sia s3, direct ip, redirect u). Tabs, nav controls, a search box, a home // page, and optional Tor onion routing. No system daemon; the app is the trust // boundary. const { app, BrowserWindow, WebContentsView, ipcMain, protocol, session, Menu, clipboard, nativeTheme, shell, dialog } = require("electron"); const path = require("path"); const http = require("http"); const https = require("https"); const tls = require("tls"); const { spawn } = require("child_process"); const fs = require("fs"); const WebSocket = require("ws"); // Packaged builds ship the resolver and tor/ as unpacked resources (they can't // run from inside app.asar); dev runs read them from the repo. const RES_DIR = app.isPackaged ? process.resourcesPath : __dirname; // THESEUS_USER_DATA points a dev run at a throwaway profile so it never // touches (or races) the real install's settings, vault and add-ons. if (process.env.THESEUS_USER_DATA) { try { app.setPath("userData", path.resolve(process.env.THESEUS_USER_DATA)); } catch (e) { console.warn("userData override failed:", e?.message); } } // Bundled as .mjs so it loads as ES module in the packaged app (no package.json // sits next to it in resources/, so a bare .js would be treated as CommonJS and // fail on `export`). Dev reads the engine copy directly (Argus is type:module). const RESOLVER = app.isPackaged ? path.join(RES_DIR, "resolver-web.mjs") : path.join(__dirname, "..", "Argus", "src", "lib", "resolver-web.js"); // Password vault β€” same .mjs-in-resources pattern as the resolver. const VAULT_MOD = app.isPackaged ? path.join(RES_DIR, "password-vault.mjs") : path.join(__dirname, "..", "Argus", "src", "lib", "password-vault.js"); let vaultLib; async function loadVaultLib() { if (!vaultLib) vaultLib = await import(`file://${VAULT_MOD.replace(/\\/g, "/")}`); return vaultLib; } // Hermes messaging module β€” same .mjs-in-resources / .js-in-dev pattern. const HERMES_MOD = app.isPackaged ? path.join(RES_DIR, "lib", "hermes.mjs") : path.join(__dirname, "lib", "hermes.js"); let hermesLib; async function loadHermesLib() { if (!hermesLib) hermesLib = await import(`file://${HERMES_MOD.replace(/\\/g, "/")}`); return hermesLib; } // Built-in engines. Users can also add their own (settings.customEngines, // each { id, name, url } where the url contains "%s" for the query). // Catalog of built-in engines (users pick which to enable + can add their own). // fav = the domain to load a real favicon from; sym = emoji fallback. // kind = "search" (traditional search engines) or "llm" (AI answer engines). // tier = "catalog" (curated first-class options shown in the primary Add // panel) or "extra" (a wider bank hidden behind a filter box for // discovery). Missing tier defaults to "catalog". // URL routing is identical for all β€” kind and tier are display-only grouping. const SEARCH_ENGINES = { duckduckgo: { kind: "search", tier: "catalog", name: "DuckDuckGo", sym: "πŸ¦†", fav: "duckduckgo.com", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) }, google: { kind: "search", tier: "catalog", name: "Google", sym: "πŸ”΅", fav: "www.google.com", url: (q, h = {}) => `https://www.google.com/search?q=${encodeURIComponent(q)}&hl=${h.hl || "en"}&gl=${h.gl || "us"}&pws=0` }, brave: { kind: "search", tier: "catalog", name: "Brave", sym: "🦁", fav: "search.brave.com", url: (q) => "https://search.brave.com/search?q=" + encodeURIComponent(q) }, bing: { kind: "search", tier: "catalog", name: "Bing", sym: "πŸ”Ž", fav: "www.bing.com", url: (q) => "https://www.bing.com/search?q=" + encodeURIComponent(q) }, startpage: { kind: "search", tier: "catalog", name: "Startpage", sym: "πŸ›‘οΈ", fav: "www.startpage.com", url: (q) => "https://www.startpage.com/sp/search?query=" + encodeURIComponent(q) }, yandex: { kind: "search", tier: "catalog", name: "Yandex", sym: "πŸ”΄", fav: "yandex.com", url: (q) => "https://yandex.com/search/?text=" + encodeURIComponent(q) }, ecosia: { kind: "search", tier: "catalog", name: "Ecosia", sym: "🌱", fav: "www.ecosia.org", url: (q) => "https://www.ecosia.org/search?q=" + encodeURIComponent(q) }, mojeek: { kind: "search", tier: "catalog", name: "Mojeek", sym: "🧭", fav: "www.mojeek.com", url: (q) => "https://www.mojeek.com/search?q=" + encodeURIComponent(q) }, // SearXNG is federated (dozens of public instances at searx.space); any single // default becomes stale as instances rate-limit / die (searx.be is anti-bot-locked). // Users who want SearXNG add their preferred instance via the custom URL form. wikipedia: { kind: "search", tier: "catalog", name: "Wikipedia", sym: "πŸ“–", fav: "en.wikipedia.org", url: (q) => "https://en.wikipedia.org/wiki/Special:Search?search=" + encodeURIComponent(q) }, // AI / LLM answer engines that ANSWER the URL query without requiring a login. // ChatGPT / Claude / You.com's youchat all bounce to sign-in before running // ?q=, so they'd fail silently as a "search engine" β€” omitted deliberately. perplexity: { kind: "llm", tier: "catalog", name: "Perplexity", sym: "🧠", fav: "www.perplexity.ai", url: (q) => "https://www.perplexity.ai/search?q=" + encodeURIComponent(q) }, phind: { kind: "llm", tier: "catalog", name: "Phind", sym: "πŸ§‘β€πŸ’»", fav: "www.phind.com", url: (q) => "https://www.phind.com/search?q=" + encodeURIComponent(q) }, // ---- Extras: wider bank, discoverable via the Search filter box in Settings. // These are known-working engines that don't require sign-in on ?q= but aren't // first-class enough to sit in the primary catalog. Keep the list vetted β€” if // an entry starts bouncing to a login gate, drop it (same rule as the LLMs). marginalia: { kind: "search", tier: "extra", name: "Marginalia", sym: "πŸ•Έ", fav: "search.marginalia.nu", url: (q) => "https://search.marginalia.nu/search?query=" + encodeURIComponent(q) }, stract: { kind: "search", tier: "extra", name: "Stract", sym: "🧡", fav: "stract.com", url: (q) => "https://stract.com/search?q=" + encodeURIComponent(q) }, yep: { kind: "search", tier: "extra", name: "Yep", sym: "✳️", fav: "yep.com", url: (q) => "https://yep.com/web?q=" + encodeURIComponent(q) }, presearch: { kind: "search", tier: "extra", name: "Presearch", sym: "πŸ”·", fav: "presearch.com", url: (q) => "https://presearch.com/search?q=" + encodeURIComponent(q) }, metager: { kind: "search", tier: "extra", name: "MetaGer", sym: "πŸ‡©πŸ‡ͺ", fav: "metager.org", url: (q) => "https://metager.org/meta/meta.ger3?eingabe=" + encodeURIComponent(q) }, qwant: { kind: "search", tier: "extra", name: "Qwant", sym: "πŸ‡«πŸ‡·", fav: "www.qwant.com", url: (q) => "https://www.qwant.com/?q=" + encodeURIComponent(q) }, swisscows: { kind: "search", tier: "extra", name: "Swisscows", sym: "πŸ„", fav: "swisscows.com", url: (q) => "https://swisscows.com/en/web?query=" + encodeURIComponent(q) }, naver: { kind: "search", tier: "extra", name: "Naver", sym: "πŸ‡°πŸ‡·", fav: "www.naver.com", url: (q) => "https://search.naver.com/search.naver?query=" + encodeURIComponent(q) }, baidu: { kind: "search", tier: "extra", name: "Baidu", sym: "πŸ‡¨πŸ‡³", fav: "www.baidu.com", url: (q) => "https://www.baidu.com/s?wd=" + encodeURIComponent(q) }, }; // Engines enabled by default (shown in the toolbar dropdown). The rest are in the // catalog and can be turned on from Settings. Custom + detected engines are always on. const DEFAULT_ENABLED = ["startpage", "duckduckgo", "google", "brave", "bing"]; // DuckDuckGo's icon service reliably returns a favicon for ANY domain from one // privacy-respecting host β€” far more robust than guessing /favicon.ico per site. const faviconUrl = (domain) => (domain ? `https://icons.duckduckgo.com/ip3/${domain}.ico` : null); function customFavicon(url) { try { return faviconUrl(new URL(String(url).replace("%s", "x")).hostname); } catch { return null; } } function isEnabled(id) { return (settings.enabledEngines || DEFAULT_ENABLED).includes(id); } // Two-tier state: an engine is INSTALLED if it's in the user's Additional // list (visible in Settings), and ENABLED if it's currently toggled on // (visible in the toolbar dropdown). Toggle flips enabled only; right-click // "Remove from list" is what actually removes an installed engine. function isInstalled(id) { if ((settings.customEngines || []).some((e) => e.id === id)) return true; // customs are always installed return (settings.installedEngines || DEFAULT_ENABLED).includes(id); } function allEngines() { const list = Object.entries(SEARCH_ENGINES).map(([id, e]) => ({ id, name: e.name, sym: e.sym, favicon: faviconUrl(e.fav), kind: e.kind || "search", tier: e.tier || "catalog", builtin: true, installed: isInstalled(id), enabled: isEnabled(id) })); for (const c of settings.customEngines || []) list.push({ id: c.id, name: c.name, sym: c.sym || "πŸ”", favicon: customFavicon(c.url), kind: c.kind || "search", tier: "custom", builtin: false, installed: true, enabled: isEnabled(c.id) }); // Apply the user's custom order; ids not in engineOrder keep their natural order (stable sort). const order = settings.engineOrder || []; return list.slice().sort((a, b) => { const ia = order.indexOf(a.id), ib = order.indexOf(b.id); if (ia === -1 && ib === -1) return 0; if (ia === -1) return 1; if (ib === -1) return -1; return ia - ib; }); } function enabledEnginesList() { return allEngines().filter((e) => e.enabled); } // Region β†’ 2-letter country code, for engines that accept a `gl`-style hint // (Google's the notable one β€” without it Google may bounce a raw ?q= URL to // a consent redirect or the region-detect start page instead of results). const REGION_TO_COUNTRY = { europe: "de", asia: "jp", north_america: "us", south_america: "br", africa: "ke", middle_east: "ae", australia: "au", }; function searchHints() { const loc = effLocale(); // e.g. "en-US" (or null β†’ show real) const region = settings.locationMode === "spoof" ? settings.locationRegion : null; return { hl: (loc || app.getLocale() || "en").split("-")[0], gl: REGION_TO_COUNTRY[region] || (loc && loc.split("-")[1]?.toLowerCase()) || "us", }; } function engineUrl(id, q) { const h = searchHints(); if (SEARCH_ENGINES[id]) return SEARCH_ENGINES[id].url(q, h); const c = (settings.customEngines || []).find((e) => e.id === id); return c ? c.url.replace(/%s/g, encodeURIComponent(q)) : SEARCH_ENGINES.duckduckgo.url(q, h); } const SEARCH = (q) => engineUrl(settings.searchEngine, q); // Public content relay (secret-free): serves s3/ip/h/u without shipping keys. const GATEWAY = "https://navigate.st"; // ---- BNS name detection (multi-TLD) -------------------------------------- // Theseus is a BNS-native browser: BCNR is the priority registry for EVERY // dotted host, regardless of TLD. The engine (resolver-web.js) resolves any //