2026-07-29 13:54:34 +02:00
// 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.
2026-08-02 11:39:00 +02:00
const { app , BrowserWindow , WebContentsView , ipcMain , protocol , session , Menu , clipboard , nativeTheme , shell } = require ( "electron" ) ;
2026-07-29 13:54:34 +02:00
const path = require ( "path" ) ;
const http = require ( "http" ) ;
const https = require ( "https" ) ;
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 ;
// 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" ) ;
2026-08-02 11:39:00 +02:00
// Built-in engines. Users can also add their own (settings.customEngines,
2026-07-30 20:58:45 +02:00
// each { id, name, url } where the url contains "%s" for the query).
2026-07-30 22:55:52 +02:00
// 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.
2026-08-02 11:39:00 +02:00
// kind = "search" (traditional search engines) or "llm" (AI answer engines).
// The picker + settings render each kind in its own section; kind stays a
// display-only grouping (URL routing is the same for both).
2026-07-30 08:16:55 +02:00
const SEARCH _ENGINES = {
2026-08-02 11:39:00 +02:00
duckduckgo : { kind : "search" , name : "DuckDuckGo" , sym : "🦆" , fav : "duckduckgo.com" , url : ( q ) => "https://duckduckgo.com/?q=" + encodeURIComponent ( q ) } ,
google : { kind : "search" , name : "Google" , sym : "🔵" , fav : "www.google.com" , url : ( q ) => "https://www.google.com/search?q=" + encodeURIComponent ( q ) } ,
brave : { kind : "search" , name : "Brave" , sym : "🦁" , fav : "search.brave.com" , url : ( q ) => "https://search.brave.com/search?q=" + encodeURIComponent ( q ) } ,
bing : { kind : "search" , name : "Bing" , sym : "🔎" , fav : "www.bing.com" , url : ( q ) => "https://www.bing.com/search?q=" + encodeURIComponent ( q ) } ,
startpage : { kind : "search" , name : "Startpage" , sym : "🛡️" , fav : "www.startpage.com" , url : ( q ) => "https://www.startpage.com/sp/search?query=" + encodeURIComponent ( q ) } ,
yandex : { kind : "search" , name : "Yandex" , sym : "🔴" , fav : "yandex.com" , url : ( q ) => "https://yandex.com/search/?text=" + encodeURIComponent ( q ) } ,
ecosia : { kind : "search" , name : "Ecosia" , sym : "🌱" , fav : "www.ecosia.org" , url : ( q ) => "https://www.ecosia.org/search?q=" + encodeURIComponent ( q ) } ,
mojeek : { kind : "search" , name : "Mojeek" , sym : "🧭" , fav : "www.mojeek.com" , url : ( q ) => "https://www.mojeek.com/search?q=" + encodeURIComponent ( q ) } ,
2026-08-02 14:43:31 +02:00
// 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.
2026-08-02 11:39:00 +02:00
wikipedia : { kind : "search" , 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" , name : "Perplexity" , sym : "🧠" , fav : "www.perplexity.ai" , url : ( q ) => "https://www.perplexity.ai/search?q=" + encodeURIComponent ( q ) } ,
phind : { kind : "llm" , name : "Phind" , sym : "🧑💻" , fav : "www.phind.com" , url : ( q ) => "https://www.phind.com/search?q=" + encodeURIComponent ( q ) } ,
2026-07-30 08:16:55 +02:00
} ;
2026-07-30 22:55:52 +02:00
// 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 = [ "duckduckgo" , "google" , "brave" , "bing" , "startpage" ] ;
2026-08-01 01:26:51 +02:00
// 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 ) ;
2026-07-30 22:55:52 +02:00
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 ) ; }
// Built-in + user-added, as flat metadata for the pickers. Built-in obey
// enabledEngines; custom engines are always enabled.
2026-07-30 20:58:45 +02:00
function allEngines ( ) {
2026-07-30 22:55:52 +02:00
const list = Object . entries ( SEARCH _ENGINES ) . map ( ( [ id , e ] ) =>
2026-08-02 11:39:00 +02:00
( { id , name : e . name , sym : e . sym , favicon : faviconUrl ( e . fav ) , kind : e . kind || "search" , builtin : true , enabled : isEnabled ( id ) } ) ) ;
2026-07-30 22:55:52 +02:00
for ( const c of settings . customEngines || [ ] )
2026-08-02 11:39:00 +02:00
list . push ( { id : c . id , name : c . name , sym : c . sym || "🔍" , favicon : customFavicon ( c . url ) , kind : c . kind || "search" , builtin : false , enabled : true } ) ;
2026-07-30 23:26:00 +02:00
// 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 ;
} ) ;
2026-07-30 20:58:45 +02:00
}
2026-07-30 22:55:52 +02:00
function enabledEnginesList ( ) { return allEngines ( ) . filter ( ( e ) => e . enabled ) ; }
2026-07-30 20:58:45 +02:00
function engineUrl ( id , q ) {
if ( SEARCH _ENGINES [ id ] ) return SEARCH _ENGINES [ id ] . url ( q ) ;
const c = ( settings . customEngines || [ ] ) . find ( ( e ) => e . id === id ) ;
return c ? c . url . replace ( /%s/g , encodeURIComponent ( q ) ) : SEARCH _ENGINES . duckduckgo . url ( q ) ;
}
const SEARCH = ( q ) => engineUrl ( settings . searchEngine , q ) ;
2026-07-29 13:54:34 +02:00
// Public content relay (secret-free): serves s3/ip/h/u without shipping keys.
const GATEWAY = "https://navigate.st" ;
// ---- BNS name detection (multi-TLD) --------------------------------------
2026-07-30 22:55:52 +02:00
// 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
// <label>.<tld> from the BCNR beacon. Flow:
// 1. User navigates to `<label>.<tld>` (address bar or link click)
// 2. Theseus asks BCNR first
// 3. If BCNR has a record — serve it (on-chain h, Sia s3, direct ip, redirect u)
// 4. If BCNR NXDOMAINs or is unreachable — fall through to the real web
// (https://<host><path>), so users aren't locked out of the clearnet
// when the chain is down or the name isn't registered.
// Non-BNS-eligible hosts (bare IPv4/IPv6, localhost, single-label hostnames,
// non-http schemes) bypass BCNR and load directly.
// The former NATIVE/DUAL sets are gone — Theseus doesn't privilege ICANN.
2026-07-29 13:54:34 +02:00
const REGISTRY = "BCNR" ; // user-facing registry label (Bitcoin Cash Name Registry)
const tldOf = ( host ) => {
const h = String ( host ) . toLowerCase ( ) . replace ( /\.$/ , "" ) ;
const dot = h . lastIndexOf ( "." ) ;
return dot < 0 ? null : h . slice ( dot + 1 ) ;
} ;
2026-07-30 22:55:52 +02:00
// Any dotted host that isn't an IP or localhost is a BCNR candidate.
const isBnsHost = ( host ) => {
if ( ! host ) return false ;
const h = String ( host ) . toLowerCase ( ) . replace ( /\.$/ , "" ) ;
if ( h === "localhost" || h . startsWith ( "localhost:" ) ) return false ;
if ( /^\d{1,3}(\.\d{1,3}){3}(:\d+)?$/ . test ( h ) ) return false ; // IPv4[:port]
if ( h . startsWith ( "[" ) ) return false ; // IPv6 literal
const dot = h . lastIndexOf ( "." ) ;
return dot > 0 && dot < h . length - 1 ; // has a real TLD
} ;
// Kept as aliases so external callers (tests, module.exports) don't break.
const nativeTld = ( host ) => isBnsHost ( host ) ? tldOf ( host ) : null ;
const dualTld = ( ) => null ; // dual-priority mode is gone — no ICANN-first TLDs
2026-07-29 13:54:34 +02:00
const registryOf = ( _tld ) => REGISTRY ;
// Address-bar heuristic: is this input a URL/hostname, or a search query? Mirrors
// what mainstream browsers do — anything with whitespace, or a bare word with no
// dot, is a search; a scheme, an IP, localhost, or a dotted host is a URL.
function looksLikeUrl ( q ) {
if ( ! q ) return false ;
if ( /\s/ . test ( q ) ) return false ; // has whitespace -> search
if ( /^[a-z][a-z0-9+.-]*:\/\//i . test ( q ) ) return true ; // scheme://…
if ( /^localhost(:\d+)?([/?#]|$)/i . test ( q ) ) return true ; // localhost[:port]
if ( /^\d{1,3}(\.\d{1,3}){3}(:\d+)?([/?#]|$)/ . test ( q ) ) return true ; // IPv4[:port]
const host = q . split ( /[/?#]/ ) [ 0 ] ; // strip path/query/frag
return host . includes ( "." ) && ! host . startsWith ( "." ) && ! host . endsWith ( "." ) ; // dotted host
}
// ---- persistent user settings (userData/settings.json) ----
const SETTINGS _DEFAULTS = {
2026-07-30 22:00:01 +02:00
webrtcMode : "public_only" , // WebRTC IP policy: default | public_only | public_private | disable_udp
2026-07-29 13:54:34 +02:00
blockCamera : true , // deny camera by default (also hides camera labels from fingerprinting)
blockMicrophone : true , // deny microphone by default (also hides mic labels)
2026-07-30 23:11:28 +02:00
hideMediaDevices : true , // blank all enumerateDevices info (esp. speaker labels/ids) like Firefox
2026-07-29 13:54:34 +02:00
restoreSession : true , // reopen last session's tabs on launch
backgroundThrottle : true , // throttle inactive tabs / the window when unfocused
2026-07-30 19:29:32 +02:00
// Anti-fingerprinting — each: show (real) | hide (neutral) | spoof (auto decoy) | manual (user value)
timezoneMode : "show" , timezoneValue : "Europe/Berlin" , // IANA zone for manual
2026-07-30 23:11:28 +02:00
languageMode : "show" , languageSpoof : "en-US" , languageValue : "en-US" , // spoof = top-10 pick, manual = free text
locationMode : "hide" , locationRegion : "europe" , locationLat : "40.7128" , locationLon : "-74.0060" , // spoof by region, or manual coords
2026-07-30 20:58:45 +02:00
searchEngine : "duckduckgo" , // default search engine (built-in id or a custom id)
2026-07-30 22:55:52 +02:00
enabledEngines : DEFAULT _ENABLED . slice ( ) , // which built-in engines show in the dropdown
2026-07-30 23:26:00 +02:00
engineOrder : [ ] , // user-defined display order of engine ids (empty = natural)
2026-07-30 20:58:45 +02:00
customEngines : [ ] , // user-added: [{ id, name, url-with-%s }]
2026-07-30 22:00:01 +02:00
theme : "dark" , // dark | light | system — drives prefers-color-scheme in all views
2026-08-01 01:26:51 +02:00
// BCNR/ICANN collision policy (see SilentMode/Argus/DESIGN-collision-modes.md):
// "bcnr-first" — BCNR wins collisions (default).
// "icann-first" — ICANN wins collisions; BCNR fills gaps.
// "soft" — "Open with…" prompt on collision, remembered per name/TLD.
collisionPolicy : "bcnr-first" ,
2026-07-29 13:54:34 +02:00
} ;
2026-07-30 22:00:01 +02:00
// Applying the theme via nativeTheme.themeSource makes prefers-color-scheme update
// in every renderer (chrome, settings, popover, page views) with no per-view IPC.
function applyTheme ( ) {
try { nativeTheme . themeSource = [ "dark" , "light" , "system" ] . includes ( settings . theme ) ? settings . theme : "dark" ; } catch { }
}
2026-07-30 19:29:32 +02:00
// Auto decoys used by "spoof" mode (plausible but not the user's real values).
const SPOOF = { tz : "America/New_York" , lang : "en-US" , lat : 40.7128 , lon : - 74.0060 } ;
2026-07-29 13:54:34 +02:00
let settings = { ... SETTINGS _DEFAULTS } ;
const settingsFile = ( ) => path . join ( app . getPath ( "userData" ) , "settings.json" ) ;
function loadSettings ( ) {
try { if ( fs . existsSync ( settingsFile ( ) ) ) settings = { ... SETTINGS _DEFAULTS , ... JSON . parse ( fs . readFileSync ( settingsFile ( ) , "utf8" ) ) } ; }
catch ( e ) { console . error ( "settings load failed:" , e . message ) ; }
}
function saveSettings ( ) {
try { fs . writeFileSync ( settingsFile ( ) , JSON . stringify ( settings , null , 2 ) ) ; } catch ( e ) { console . error ( "settings save failed:" , e . message ) ; }
}
2026-07-30 08:16:55 +02:00
// ---- bookmarks / saved pages (userData/bookmarks.json) ----
let bookmarks = [ ] ;
const bookmarksFile = ( ) => path . join ( app . getPath ( "userData" ) , "bookmarks.json" ) ;
function loadBookmarks ( ) { try { if ( fs . existsSync ( bookmarksFile ( ) ) ) bookmarks = JSON . parse ( fs . readFileSync ( bookmarksFile ( ) , "utf8" ) ) ; } catch ( e ) { console . error ( "bookmarks load failed:" , e . message ) ; } }
function saveBookmarks ( ) { try { fs . writeFileSync ( bookmarksFile ( ) , JSON . stringify ( bookmarks , null , 2 ) ) ; } catch ( e ) { console . error ( "bookmarks save failed:" , e . message ) ; } }
function emitBookmarks ( ) { try { chrome ? . webContents . send ( "bookmarks" , bookmarks ) ; } catch { } }
2026-08-01 01:26:51 +02:00
// ---- BCNR/ICANN collisions (userData/collisions.json) --------------------
// Per-name / per-TLD "always use X" overrides for soft "Open with…" mode.
// See D:\Dev\SilentMode\Argus\DESIGN-collision-modes.md for the full model.
let collisions = { byName : { } , byTld : { } } ;
const collisionsFile = ( ) => path . join ( app . getPath ( "userData" ) , "collisions.json" ) ;
function loadCollisions ( ) {
try { if ( fs . existsSync ( collisionsFile ( ) ) ) collisions = { byName : { } , byTld : { } , ... JSON . parse ( fs . readFileSync ( collisionsFile ( ) , "utf8" ) ) } ; }
catch ( e ) { console . error ( "collisions load failed:" , e . message ) ; }
}
function saveCollisions ( ) {
try { fs . writeFileSync ( collisionsFile ( ) , JSON . stringify ( collisions , null , 2 ) ) ; } catch ( e ) { console . error ( "collisions save failed:" , e . message ) ; }
}
// per-name > per-TLD > hard policy. Returns "bcnr" | "icann" | null (null = ask in soft).
function overrideFor ( host , tld ) {
const n = String ( host ) . toLowerCase ( ) ;
const t = String ( tld || "" ) . toLowerCase ( ) ;
if ( collisions . byName [ n ] ) return collisions . byName [ n ] ;
if ( collisions . byTld [ t ] ) return collisions . byTld [ t ] ;
return null ;
}
// Cached BCNR-native TLD list from tlds.bch. Registered names under a native TLD
// are NOT collision candidates (whole TLD belongs to BCNR); non-native = might collide.
let bcnrTlds = [ "bch" ] ;
function isBcnrNativeTld ( tld ) { return bcnrTlds . includes ( String ( tld || "" ) . toLowerCase ( ) ) ; }
function refreshBcnrTlds ( index ) {
try {
const raw = index ? . get ? . ( "tlds.bch" ) ? . records ? . tlds ;
if ( typeof raw === "string" ) {
const list = raw . split ( /\s+/ ) . filter ( Boolean ) . map ( ( s ) => s . toLowerCase ( ) ) ;
if ( list . length ) bcnrTlds = list ;
}
} catch { }
}
2026-08-02 14:43:31 +02:00
// (The old modal-based collisionPromptOnce() was removed 2026-08-02 — the
// prompt is now an in-tab full-page interstitial loaded from collision.html,
// wired via the bns://collision-choose/ handler in serveBns().)
2026-08-01 01:26:51 +02:00
function rememberCollision ( host , tld , choice , remember ) {
if ( choice !== "bcnr" && choice !== "icann" ) return ;
if ( remember === "name" ) collisions . byName [ String ( host ) . toLowerCase ( ) ] = choice ;
else if ( remember === "tld" ) collisions . byTld [ String ( tld || "" ) . toLowerCase ( ) ] = choice ;
if ( remember !== "no" ) saveCollisions ( ) ;
}
2026-07-30 22:55:52 +02:00
function emitEngines ( ) {
try { chrome ? . webContents . send ( "engines" , { engines : enabledEnginesList ( ) , current : settings . searchEngine } ) ; } catch { }
if ( epVisible ) try { enginePicker ? . webContents . send ( "engines" , { engines : enabledEnginesList ( ) , current : settings . searchEngine , detected : activeTab ( ) ? . detected || null } ) ; } catch { }
}
2026-07-30 22:00:01 +02:00
// WebRTC IP-handling policy — the same control the "WebRTC Network Limiter"
// Chrome extension provides, done natively (that extension's chrome.privacy API
// isn't available in Electron, and this is more reliable). Tor forces the strongest.
const WEBRTC _POLICIES = {
default : "default" , // allow all (may expose local IP)
public _only : "default_public_interface_only" , // only the default public interface
public _private : "default_public_and_private_interfaces" ,
disable _udp : "disable_non_proxied_udp" , // strongest (only proxied UDP)
} ;
2026-07-29 13:54:34 +02:00
function webrtcPolicy ( ) {
2026-07-30 22:00:01 +02:00
if ( torState === "on" ) return "disable_non_proxied_udp" ;
return WEBRTC _POLICIES [ settings . webrtcMode ] || "default_public_interface_only" ;
2026-07-29 13:54:34 +02:00
}
2026-07-30 19:29:32 +02:00
// ---- anti-fingerprinting: timezone + language + location ----
// Show/Hide/Spoof/Manual. Effective override, or null = "show" (real value).
2026-07-29 13:54:34 +02:00
function effTimezone ( ) {
2026-07-30 19:29:32 +02:00
switch ( settings . timezoneMode ) {
case "hide" : return "UTC" ;
case "spoof" : return SPOOF . tz ;
case "manual" : return settings . timezoneValue || "UTC" ;
default : return null ;
}
2026-07-29 13:54:34 +02:00
}
function effLocale ( ) {
2026-07-30 19:29:32 +02:00
switch ( settings . languageMode ) {
case "hide" : return "en-US" ;
2026-07-30 23:11:28 +02:00
case "spoof" : return settings . languageSpoof || SPOOF . lang ; // chosen from the top-languages list
2026-07-30 19:29:32 +02:00
case "manual" : return settings . languageValue || "en-US" ;
default : return null ;
}
}
2026-07-30 23:11:28 +02:00
// Representative coordinates per world region — used when the spoofed location is
// set to a region rather than exact coordinates (a major city stands in for each).
const REGIONS = {
europe : { lat : 52.5200 , lon : 13.4050 } , // Berlin
asia : { lat : 35.6762 , lon : 139.6503 } , // Tokyo
north _america : { lat : 40.7128 , lon : - 74.0060 } , // New York
south _america : { lat : - 23.5505 , lon : - 46.6333 } , // São Paulo
africa : { lat : - 1.2921 , lon : 36.8219 } , // Nairobi
middle _east : { lat : 25.2048 , lon : 55.2708 } , // Dubai
australia : { lat : - 33.8688 , lon : 151.2093 } , // Sydney
} ;
2026-07-30 19:29:32 +02:00
// Geolocation: null = show (real, allowed); "deny" = hide (blocked);
2026-07-30 23:11:28 +02:00
// {lat,lon} = spoof (region-based) / manual (exact) — overridden in-page.
2026-07-30 19:29:32 +02:00
function effLocation ( ) {
const m = settings . locationMode ;
if ( m === "hide" ) return "deny" ;
2026-07-30 23:11:28 +02:00
if ( m === "spoof" ) { const r = REGIONS [ settings . locationRegion ] || REGIONS . europe ; return { lat : r . lat , lon : r . lon } ; }
2026-07-30 19:29:32 +02:00
if ( m === "manual" ) return { lat : Number ( settings . locationLat ) || 0 , lon : Number ( settings . locationLon ) || 0 } ;
return null ; // show
2026-07-29 13:54:34 +02:00
}
// Applied per tab via CDP — the engine-level override the Tor/Mullvad browsers do:
// timezone -> Intl/Date; locale -> Intl + navigator.language(s).
async function applyFingerprint ( wc ) {
try {
if ( ! wc . debugger . isAttached ( ) ) wc . debugger . attach ( "1.3" ) ;
const tz = effTimezone ( ) ;
await wc . debugger . sendCommand ( "Emulation.setTimezoneOverride" , { timezoneId : tz || "" } ) ;
const loc = effLocale ( ) ;
await wc . debugger . sendCommand ( "Emulation.setLocaleOverride" , loc ? { locale : loc } : { } ) ;
// setLocaleOverride covers Intl but NOT navigator.language(s) — inject a getter.
await wc . debugger . sendCommand ( "Page.enable" ) ;
if ( wc . _langScript ) {
try { await wc . debugger . sendCommand ( "Page.removeScriptToEvaluateOnNewDocument" , { identifier : wc . _langScript } ) ; } catch { }
wc . _langScript = null ;
}
2026-07-30 19:29:32 +02:00
// Build one injected script covering navigator.language(s) and geolocation.
let src = "" ;
2026-07-29 13:54:34 +02:00
if ( loc ) {
const langs = JSON . stringify ( [ loc , loc . split ( "-" ) [ 0 ] ] ) ;
2026-07-30 19:29:32 +02:00
src += ` Object.defineProperty(navigator,'language',{get:()=> ${ JSON . stringify ( loc ) } ,configurable:true}); ` +
` Object.defineProperty(navigator,'languages',{get:()=> ${ langs } ,configurable:true}); ` ;
}
const geo = effLocation ( ) ;
if ( geo && geo !== "deny" ) { // spoof/manual: override the reported coordinates
const pos = ` {coords:{latitude: ${ geo . lat } ,longitude: ${ geo . lon } ,accuracy:100,altitude:null,altitudeAccuracy:null,heading:null,speed:null},timestamp:Date.now()} ` ;
src += ` try{const p=()=>( ${ pos } );if(navigator.geolocation){navigator.geolocation.getCurrentPosition=(ok)=>{try{ok(p())}catch(e){}};navigator.geolocation.watchPosition=(ok)=>{try{ok(p())}catch(e){}return 0};}}catch(e){} ` ;
}
2026-07-30 23:11:28 +02:00
// Media-device privacy: Chromium leaks audiooutput (speaker) labels + deviceIds
// via enumerateDevices even when camera/mic are blocked. Like Firefox, blank
// every device's label/deviceId/groupId and collapse to one entry per kind.
if ( settings . hideMediaDevices ) {
src += ` try{const md=navigator.mediaDevices;if(md&&md.enumerateDevices){const o=md.enumerateDevices.bind(md);md.enumerateDevices=async()=>{let l=[];try{l=await o()}catch(e){}const ks=[...new Set(l.map(d=>d.kind))];return ks.map(kind=>({deviceId:'',kind:kind,label:'',groupId:'',toJSON(){return{deviceId:'',kind:kind,label:'',groupId:''}}}))};}}catch(e){} ` ;
}
2026-07-30 19:29:32 +02:00
if ( src ) {
2026-07-29 13:54:34 +02:00
const res = await wc . debugger . sendCommand ( "Page.addScriptToEvaluateOnNewDocument" , { source : src } ) ;
wc . _langScript = res . identifier ;
try { await wc . executeJavaScript ( src ) ; } catch { } // apply to the current page too
}
} catch { /* debugger busy (e.g. devtools) — best effort */ }
}
function applyFingerprintAll ( ) { for ( const t of tabs ) applyFingerprint ( t . view . webContents ) ; }
// Accept-Language header follows the locale setting (session-wide, best effort).
function applyAcceptLanguage ( ) {
const loc = effLocale ( ) || app . getLocale ( ) || "en-US" ;
try {
const ua = session . defaultSession . getUserAgent ( ) ;
session . defaultSession . setUserAgent ( ua , ` ${ loc } , ${ loc . split ( "-" ) [ 0 ] } ;q=0.8 ` ) ;
} catch { }
}
// ---- session restore + background throttling ----
const sessionFile = ( ) => path . join ( app . getPath ( "userData" ) , "session.json" ) ;
function saveSession ( ) {
try { fs . writeFileSync ( sessionFile ( ) , JSON . stringify ( tabs . filter ( ( t ) => ! t . settings && t . url ) . map ( ( t ) => t . url ) ) ) ; }
catch ( e ) { console . error ( "session save failed:" , e . message ) ; }
}
function loadSession ( ) {
try { if ( fs . existsSync ( sessionFile ( ) ) ) return JSON . parse ( fs . readFileSync ( sessionFile ( ) , "utf8" ) ) ; } catch { }
return [ ] ;
}
function applyThrottle ( ) {
for ( const t of tabs ) { try { t . view . webContents . setBackgroundThrottling ( settings . backgroundThrottle ) ; } catch { } }
}
// Privacy-first permissions: Electron auto-grants everything by default. Deny the
// sensitive ones (camera/mic/geolocation/device access) — this also hides real
// media-device labels/ids from enumerateDevices. Handlers read settings live.
// Device permissions with no legitimate need here — always denied.
const SENSITIVE _DEVICE = new Set ( [ "hid" , "serial" , "usb" , "bluetooth" , "midi" , "midiSysex" ] ) ;
// A "media" request may ask for audio, video, or both — allow only if none blocked.
function mediaAllowed ( kinds ) {
if ( kinds . includes ( "video" ) && settings . blockCamera ) return false ;
if ( kinds . includes ( "audio" ) && settings . blockMicrophone ) return false ;
return true ;
}
function applyPermissions ( ) {
const ses = session . defaultSession ;
ses . setPermissionRequestHandler ( ( _wc , permission , callback , details ) => {
if ( permission === "media" ) return callback ( mediaAllowed ( details ? . mediaTypes || [ ] ) ) ;
2026-07-30 19:29:32 +02:00
if ( permission === "geolocation" ) return callback ( effLocation ( ) !== "deny" ) ; // allow unless "hide"
2026-07-29 13:54:34 +02:00
if ( SENSITIVE _DEVICE . has ( permission ) ) return callback ( false ) ;
callback ( true ) ; // benign UX permissions (fullscreen, pointerLock, …)
} ) ;
ses . setPermissionCheckHandler ( ( _wc , permission , _origin , details ) => {
if ( permission === "media" ) {
if ( details ? . mediaType === "video" ) return ! settings . blockCamera ;
if ( details ? . mediaType === "audio" ) return ! settings . blockMicrophone ;
return ! ( settings . blockCamera && settings . blockMicrophone ) ;
}
2026-07-30 19:29:32 +02:00
if ( permission === "geolocation" ) return effLocation ( ) !== "deny" ;
2026-07-29 13:54:34 +02:00
if ( SENSITIVE _DEVICE . has ( permission ) ) return false ;
return true ;
} ) ;
}
protocol . registerSchemesAsPrivileged ( [
{ scheme : "bns" , privileges : { standard : true , secure : true , supportFetchAPI : true , stream : true } } ,
] ) ;
let resolver ;
async function getResolver ( ) {
if ( ! resolver ) resolver = await import ( ` file:// ${ RESOLVER . replace ( /\\/g , "/" ) } ` ) ;
return resolver ;
}
// ---- Tor (optional onion routing, toggled from the UI) ----
// IP privacy, not full anonymity: this browser can still be fingerprinted.
const TOR _PORT = 9152 ;
const TOR _BIN = path . join ( RES _DIR , "tor" , "tor" , "tor.exe" ) ;
const TOR _GEOIP = path . join ( RES _DIR , "tor" , "data" , "geoip" ) ;
const TOR _GEOIP6 = path . join ( RES _DIR , "tor" , "data" , "geoip6" ) ;
let torProc = null , torState = "off" ;
let torWsAgent = null ;
let SocksProxyAgent ;
async function loadSocks ( ) { if ( ! SocksProxyAgent ) ( { SocksProxyAgent } = await import ( "socks-proxy-agent" ) ) ; }
function sendTor ( ) { try { chrome ? . webContents . send ( "tor" , { state : torState } ) ; } catch { } }
async function startTor ( ) {
if ( torProc ) return ;
torState = "connecting" ; sendTor ( ) ;
await loadSocks ( ) ;
const dataDir = path . join ( app . getPath ( "userData" ) , "tor-data" ) ;
torProc = spawn ( TOR _BIN , [ "--SocksPort" , String ( TOR _PORT ) , "--ControlPort" , "0" ,
"--DataDirectory" , dataDir , "--GeoIPFile" , TOR _GEOIP , "--GeoIPv6File" , TOR _GEOIP6 ] , { windowsHide : true } ) ;
torProc . stdout . on ( "data" , ( d ) => { if ( /Bootstrapped 100%/ . test ( d . toString ( ) ) ) torReady ( ) ; } ) ;
torProc . stderr . on ( "data" , ( ) => { } ) ;
torProc . on ( "exit" , ( ) => { torProc = null ; if ( torState !== "off" ) torOff ( ) ; } ) ;
}
function torReady ( ) {
torState = "on" ;
torWsAgent = new SocksProxyAgent ( ` socks5h://127.0.0.1: ${ TOR _PORT } ` ) ;
session . defaultSession . setProxy ( { proxyRules : ` socks5://127.0.0.1: ${ TOR _PORT } ` } ) ;
applyWebRTCPolicy ( ) ;
sendTor ( ) ;
}
function torOff ( ) {
torState = "off" ; torWsAgent = null ;
session . defaultSession . setProxy ( { proxyRules : "" } ) ;
applyWebRTCPolicy ( ) ;
sendTor ( ) ;
}
function stopTor ( ) { torOff ( ) ; if ( torProc ) { try { torProc . kill ( ) ; } catch { } torProc = null ; } }
// While Tor is on, stop WebRTC from leaking the real IP around the SOCKS proxy
// (STUN/UDP bypasses an HTTP/SOCKS proxy — plain Electron doesn't block it the
// way the Tor Browser does). This is the usual reason a site still sees your IP.
function applyWebRTCPolicy ( ) {
const policy = webrtcPolicy ( ) ;
for ( const t of tabs ) { try { t . view . webContents . setWebRTCIPHandlingPolicy ( policy ) ; } catch { } }
}
class TorWebSocket extends WebSocket { constructor ( url , opts ) { super ( url , { agent : torWsAgent , ... opts } ) ; } }
const currentWS = ( ) => ( torState === "on" ? TorWebSocket : WebSocket ) ;
function nodeRequest ( urlStr , { method = "GET" , headers = { } , agent } = { } ) {
return new Promise ( ( resolve , reject ) => {
const u = new URL ( urlStr ) ;
const lib = u . protocol === "https:" ? https : http ;
const req = lib . request ( u , { method , headers , agent } , ( res ) => {
const chunks = [ ] ;
res . on ( "data" , ( c ) => chunks . push ( c ) ) ;
res . on ( "end" , ( ) => resolve ( { status : res . statusCode , contentType : res . headers [ "content-type" ] , buffer : Buffer . concat ( chunks ) } ) ) ;
} ) ;
req . on ( "error" , reject ) ; req . end ( ) ;
} ) ;
}
async function contentFetch ( url , init = { } ) {
if ( torState === "on" ) { await loadSocks ( ) ; return nodeRequest ( url , { ... init , agent : new SocksProxyAgent ( ` socks5h://127.0.0.1: ${ TOR _PORT } ` ) } ) ; }
const r = await fetch ( url , init ) ;
return { status : r . status , contentType : r . headers . get ( "content-type" ) , buffer : Buffer . from ( await r . arrayBuffer ( ) ) } ;
}
2026-07-30 22:55:52 +02:00
// OpenSearch "scan": fetch a page's OpenSearch description and turn its HTML
// search template into our { name, url-with-%s } form.
async function fetchOpenSearch ( href ) {
try {
const r = await contentFetch ( href , { } ) ;
const xml = r . buffer . toString ( "utf8" ) ;
const nameM = xml . match ( /<ShortName>([^<]+)<\/ShortName>/i ) ;
const urlM = xml . match ( /<Url\b[^>]*type=["']text\/html["'][^>]*template=["']([^"']+)["']/i )
|| xml . match ( /<Url\b[^>]*template=["']([^"']+)["'][^>]*type=["']text\/html["']/i ) ;
if ( ! urlM ) return null ;
const template = urlM [ 1 ] . replace ( /\{searchTerms\??\}/gi , "%s" ) . replace ( /\{[^}]*\}/g , "" ) ; // drop other {params}
if ( ! template . includes ( "%s" ) || ! /^https?:\/\//i . test ( template ) ) return null ;
return { name : ( nameM ? nameM [ 1 ] : new URL ( href ) . hostname ) . trim ( ) . slice ( 0 , 40 ) , url : template } ;
} catch { return null ; }
}
2026-07-29 13:54:34 +02:00
// ---- electrum server pool: hardcoded seed + on-chain discovery, persisted ----
// Bootstrap from the baked-in seed (with pinned IPs), then refresh from the
// on-chain ELECTRUM_LIST_NAME record so the pool can be rotated without a new
// build. The last discovered list is cached to disk and tried first next launch.
let electrumPool = null ;
let lastElectrumRefresh = 0 ;
const electrumFile = ( ) => path . join ( app . getPath ( "userData" ) , "electrum-servers.json" ) ;
const serverKey = ( s ) => ( typeof s === "string" ? s : s && s . url ) ;
function mergeServers ( preferred , rest ) {
const seen = new Set ( ) , out = [ ] ;
for ( const s of [ ... ( preferred || [ ] ) , ... ( rest || [ ] ) ] ) {
const k = serverKey ( s ) ;
if ( k && ! seen . has ( k ) ) { seen . add ( k ) ; out . push ( s ) ; }
}
return out ;
}
async function initElectrumPool ( ) {
const { CHIPNET _ELECTRUM } = await getResolver ( ) ;
let saved = [ ] ;
try { if ( fs . existsSync ( electrumFile ( ) ) ) saved = JSON . parse ( fs . readFileSync ( electrumFile ( ) , "utf8" ) ) ; } catch { }
electrumPool = mergeServers ( saved , CHIPNET _ELECTRUM ) ; // discovered first, seed always kept
}
async function refreshElectrumPool ( ) {
try {
const { fetchElectrumServers } = await getResolver ( ) ;
const found = await fetchElectrumServers ( { WebSocket : currentWS ( ) , directIP : true , electrum : electrumPool } ) ;
if ( found && found . length ) {
electrumPool = mergeServers ( found , electrumPool ) ;
try { fs . writeFileSync ( electrumFile ( ) , JSON . stringify ( found , null , 2 ) ) ; } catch { }
}
} catch { /* list unpublished or unreachable — keep the current pool */ }
}
function maybeRefreshElectrum ( ) {
if ( Date . now ( ) - lastElectrumRefresh < 30 * 60 * 1000 ) return ;
lastElectrumRefresh = Date . now ( ) ;
refreshElectrumPool ( ) ; // fire-and-forget
}
const entries = new Map ( ) ;
2026-07-30 19:29:32 +02:00
// Cached chain index. Building it (connect + fetch every beacon tx) is the slow
// part, and it was happening on EVERY navigation. Build once, reuse for lookups,
// and refresh in the background — so .bch pages open near-instantly after the first.
let sharedIndex = null , indexBuiltAt = 0 , indexBuilding = null ;
const INDEX _TTL = 45_000 ;
async function ensureIndex ( force = false ) {
const { buildIndex } = await getResolver ( ) ;
2026-07-29 13:54:34 +02:00
if ( ! electrumPool ) await initElectrumPool ( ) ;
2026-07-30 19:29:32 +02:00
if ( ! force && sharedIndex && Date . now ( ) - indexBuiltAt < INDEX _TTL ) return sharedIndex ;
if ( indexBuilding ) return indexBuilding ; // dedupe concurrent builds
indexBuilding = buildIndex ( { WebSocket : currentWS ( ) , directIP : true , electrum : electrumPool } )
2026-08-02 11:39:00 +02:00
. then ( ( idx ) => { sharedIndex = idx ; indexBuiltAt = Date . now ( ) ; refreshBcnrTlds ( idx ) ; return idx ; } )
2026-07-30 19:29:32 +02:00
. finally ( ( ) => { indexBuilding = null ; } ) ;
// If we have a stale index, don't block on the rebuild — serve stale, refresh async.
return ( sharedIndex && ! force ) ? sharedIndex : indexBuilding ;
}
async function resolveHost ( host ) {
const { normalizeName } = await getResolver ( ) ;
let key ; try { key = normalizeName ( host ) ; } catch { return null ; }
let idx = await ensureIndex ( ) ;
let entry = idx . get ( key ) ? ? null ;
// Miss on a possibly-stale index → one fresh build (the name may be newly registered).
if ( ! entry && Date . now ( ) - indexBuiltAt > 8_000 ) { idx = await ensureIndex ( true ) ; entry = idx . get ( key ) ? ? null ; }
2026-07-29 13:54:34 +02:00
if ( entry ) entries . set ( host . toLowerCase ( ) , { entry , host : host . toLowerCase ( ) } ) ;
maybeRefreshElectrum ( ) ;
return entry ;
}
const MIME = { html : "text/html; charset=utf-8" , htm : "text/html; charset=utf-8" , css : "text/css" , js : "text/javascript" ,
json : "application/json" , png : "image/png" , jpg : "image/jpeg" , jpeg : "image/jpeg" , gif : "image/gif" , svg : "image/svg+xml" ,
ico : "image/x-icon" , webp : "image/webp" , woff2 : "font/woff2" , woff : "font/woff" , txt : "text/plain" , wasm : "application/wasm" } ;
const guessType = ( p ) => MIME [ p . split ( "." ) . pop ( ) ? . toLowerCase ( ) ] || "application/octet-stream" ;
async function serveBns ( request ) {
const url = new URL ( request . url ) ;
const host = url . hostname . toLowerCase ( ) ;
const reqPath = decodeURIComponent ( url . pathname ) || "/" ;
2026-08-02 14:43:31 +02:00
// Special host: apply a soft-mode "Open with…" choice submitted by
// collision.html, then redirect the tab to the actual target. Persistent
// memory (byName/byTld) is written HERE; the one-shot BCDN choice rides
// along via a ?_collision=bcnr param that loadBns consumes.
if ( host === "collision-choose" ) {
const p = url . searchParams ;
const target = String ( p . get ( "host" ) || "" ) . toLowerCase ( ) ;
const cTld = String ( p . get ( "tld" ) || "" ) . toLowerCase ( ) ;
const cChoice = p . get ( "choice" ) ;
const cRem = p . get ( "remember" ) || "no" ;
const rest = p . get ( "resturl" ) || "/" ;
if ( cChoice === "bcnr" || cChoice === "icann" ) {
rememberCollision ( target , cTld , cChoice , cRem ) ;
}
let dest ;
if ( cChoice === "icann" ) {
dest = "https://" + target + rest ;
} else {
// BCNR/BCDN: bounce back through the bns:// resolver with a one-shot
// marker so loadBns won't re-prompt for this specific load.
const sep = rest . includes ( "?" ) ? "&" : "?" ;
dest = "bns://" + target + rest + sep + "_collision=bcnr" ;
}
const html = ` <!doctype html><meta charset="utf-8"><title>Opening…</title> ` +
` <meta http-equiv="refresh" content="0;url= ${ dest . replace ( /"/g , """ ) } "> ` +
` <body style="background:#0f1116;color:#9ca3af;font:14px system-ui;padding:24px">Opening ${ target } …</body> ` ;
return new Response ( html , { headers : { "content-type" : "text/html; charset=utf-8" } } ) ;
}
2026-07-29 13:54:34 +02:00
let rec = entries . get ( host ) ;
if ( ! rec ) { try { await resolveHost ( host ) ; } catch { } rec = entries . get ( host ) ; }
if ( ! rec ) return new Response ( "NXDOMAIN: " + host , { status : 404 , headers : { "content-type" : "text/plain" } } ) ;
const r = rec . entry . records ;
try {
if ( r . h ) { if ( reqPath === "/" ) return new Response ( r . h , { headers : { "content-type" : "text/html; charset=utf-8" } } ) ; return new Response ( "not found" , { status : 404 } ) ; }
if ( r . s3 ) {
// Secret-free: fetch Sia content from the public gateway (it holds the
// keys and owns the subfolder mapping) instead of signing S3 requests
// with credentials that must never ship in a public build.
const up = await contentFetch ( ` ${ GATEWAY } /bns/ ${ host } ${ reqPath } ${ url . search } ` , { } ) ;
const ct = up . contentType && up . contentType !== "application/octet-stream"
? up . contentType : guessType ( reqPath === "/" ? "index.html" : reqPath ) ;
let body = up . buffer ;
if ( ct . includes ( "text/html" ) ) {
// Strip the gateway's path-form <base href="/bns/<name>/"> so assets
// resolve against the bns:// origin, not back through the relay.
body = Buffer . from ( body . toString ( "utf8" ) . replace ( /<base\s+href="\/bns\/[^"]*">/i , "" ) , "utf8" ) ;
}
return new Response ( body , { status : up . status , headers : { "content-type" : ct } } ) ;
}
if ( r . ip ) {
const up = await contentFetch ( ` http:// ${ r . ip } ${ reqPath } ${ url . search } ` , { headers : { host } } ) ;
return new Response ( up . buffer , { status : up . status , headers : { "content-type" : up . contentType || guessType ( reqPath ) } } ) ;
}
if ( r . u ) return Response . redirect ( r . u , 302 ) ;
return new Response ( JSON . stringify ( rec . entry , null , 2 ) , { headers : { "content-type" : "application/json" } } ) ;
} catch ( e ) { return new Response ( "Theseus error: " + e . message , { status : 502 } ) ; }
}
// ---- window + tabs ----
2026-07-30 19:29:32 +02:00
let win , chrome ;
2026-07-30 08:16:55 +02:00
let CHROME _H = 84 ; // grows when an extra bar (Tor notice / BCNR offer) is shown
// Site-info popover: a floating overlay VIEW on top of the page content, so it
// never pushes the page down. Positioned under the address-bar badge on demand.
let popover , popVisible = false , popPos = { x : 8 , y : 90 } ;
2026-07-30 20:58:45 +02:00
const POP _W = 360 ; let popH = 210 ; // popH is updated to fit the popover's content
2026-07-30 22:55:52 +02:00
// Engine-picker: a second floating overlay VIEW (a custom dropdown that shows real
// engine favicons, like Firefox — a native <select> can't render images).
let enginePicker , epVisible = false , epPos = { x : 8 , y : 90 } ;
const EP _W = 250 ; let epH = 320 ;
2026-08-02 11:39:00 +02:00
// Downloads popover — a third floating overlay VIEW showing in-flight and
// recently-finished downloads. Anchored under the toolbar's download button.
let downloadsPop , dlVisible = false , dlPos = { x : 8 , y : 90 } ;
const DL _W = 340 ; let dlH = 240 ;
// In-memory download list. Not persisted: closing the browser clears history
// (the files are still on disk; only the list of "recent downloads" is dropped).
const downloads = [ ] ; let nextDlId = 1 ; const dlItems = new Map ( ) ; // id -> DownloadItem
2026-07-29 13:54:34 +02:00
const tabs = [ ] ; // { id, view, title, url, prov }
let activeId = null , tabSeq = 0 ;
const tabById = ( id ) => tabs . find ( ( t ) => t . id === id ) ;
const activeTab = ( ) => tabById ( activeId ) ;
// Provenance goes to BOTH the top chrome (registry badge + site-info panel) and
// the bottom status line, so the resolver detail lives on the bottom bar.
2026-08-02 11:39:00 +02:00
// Enrich prov with a "collision candidate?" flag so the popover switcher can
// know whether flipping BCNR<->ICANN is meaningful. A BCNR-native TLD (in
// tlds.bch) is NOT a collision candidate — the whole TLD is BCNR's.
function decorate ( prov ) {
if ( ! prov || ! prov . tld ) return prov ;
return { ... prov , bcnrNativeTld : isBcnrNativeTld ( prov . tld ) } ;
}
2026-07-29 13:54:34 +02:00
function pushNav ( prov ) {
2026-08-02 11:39:00 +02:00
const p = decorate ( prov ) ;
chrome ? . webContents . send ( "nav" , p ) ;
if ( popVisible ) popover ? . webContents . send ( "site-info" , p ) ;
2026-07-29 13:54:34 +02:00
}
function layout ( ) {
if ( ! win ) return ;
const { width , height } = win . getContentBounds ( ) ;
chrome . setBounds ( { x : 0 , y : 0 , width , height : CHROME _H } ) ;
2026-07-30 19:29:32 +02:00
const bodyH = Math . max ( 0 , height - CHROME _H ) ;
2026-07-29 13:54:34 +02:00
for ( const t of tabs ) t . view . setBounds ( { x : 0 , y : CHROME _H , width , height : bodyH } ) ;
2026-07-30 08:16:55 +02:00
positionPopover ( ) ;
2026-07-30 22:55:52 +02:00
positionEnginePicker ( ) ;
2026-08-02 11:39:00 +02:00
positionDownloads ( ) ;
2026-07-30 08:16:55 +02:00
}
function positionPopover ( ) {
if ( ! popover ) return ;
const { width } = win . getContentBounds ( ) ;
const x = Math . max ( 6 , Math . min ( popPos . x , width - POP _W - 6 ) ) ;
2026-07-30 20:58:45 +02:00
popover . setBounds ( { x , y : popPos . y , width : POP _W , height : popH } ) ;
2026-07-30 08:16:55 +02:00
}
function showPopover ( show ) {
if ( ! popover ) return ;
if ( show ) {
positionPopover ( ) ;
// Re-add to the top of the z-order (tabs added later would otherwise cover it).
win . contentView . removeChildView ( popover ) ;
win . contentView . addChildView ( popover ) ;
popover . setVisible ( true ) ; popVisible = true ;
2026-08-02 11:39:00 +02:00
popover . webContents . send ( "site-info" , decorate ( activeTab ( ) ? . prov ) || { kind : "home" } ) ;
2026-07-30 08:16:55 +02:00
} else { popover . setVisible ( false ) ; popVisible = false ; }
2026-07-29 13:54:34 +02:00
}
2026-07-30 22:55:52 +02:00
function positionEnginePicker ( ) {
if ( ! enginePicker ) return ;
const { width } = win . getContentBounds ( ) ;
const x = Math . max ( 6 , Math . min ( epPos . x , width - EP _W - 6 ) ) ;
enginePicker . setBounds ( { x , y : epPos . y , width : EP _W , height : epH } ) ;
}
function showEnginePicker ( show ) {
if ( ! enginePicker ) return ;
if ( show ) {
positionEnginePicker ( ) ;
win . contentView . removeChildView ( enginePicker ) ;
win . contentView . addChildView ( enginePicker ) ;
enginePicker . setVisible ( true ) ; epVisible = true ;
enginePicker . webContents . send ( "engines" , { engines : enabledEnginesList ( ) , current : settings . searchEngine , detected : activeTab ( ) ? . detected || null } ) ;
} else { enginePicker . setVisible ( false ) ; epVisible = false ; }
}
2026-08-02 11:39:00 +02:00
function positionDownloads ( ) {
if ( ! downloadsPop ) return ;
const { width } = win . getContentBounds ( ) ;
const x = Math . max ( 6 , Math . min ( dlPos . x , width - DL _W - 6 ) ) ;
downloadsPop . setBounds ( { x , y : dlPos . y , width : DL _W , height : dlH } ) ;
}
function showDownloads ( show ) {
if ( ! downloadsPop ) return ;
if ( show ) {
positionDownloads ( ) ;
win . contentView . removeChildView ( downloadsPop ) ;
win . contentView . addChildView ( downloadsPop ) ;
downloadsPop . setVisible ( true ) ; dlVisible = true ;
downloadsPop . webContents . send ( "downloads" , downloadsPublic ( ) ) ;
} else { downloadsPop . setVisible ( false ) ; dlVisible = false ; }
}
// Public view of a download — no DownloadItem refs leak to the renderer.
const downloadsPublic = ( ) => downloads . map ( ( d ) => ( { ... d } ) ) ;
function emitDownloads ( ) {
const pub = downloadsPublic ( ) ;
try { chrome ? . webContents . send ( "downloads" , pub ) ; } catch { }
if ( dlVisible ) try { downloadsPop ? . webContents . send ( "downloads" , pub ) ; } catch { }
}
// Attach the will-download listener to the SHARED default session. Every tab's
// WebContents inherits it, so we catch downloads regardless of which tab
// initiated them (including anchor clicks with `download`, form posts serving
// attachments, and manual save-as gestures).
function installDownloadTracker ( ) {
session . defaultSession . on ( "will-download" , ( _e , item /*, wc */ ) => {
const id = nextDlId ++ ;
const rec = {
id ,
filename : item . getFilename ( ) ,
url : item . getURL ( ) ,
mime : item . getMimeType ( ) ,
total : item . getTotalBytes ( ) || 0 ,
received : 0 ,
state : "progressing" , // progressing | paused | completed | cancelled | interrupted
savePath : "" ,
startedAt : Date . now ( ) ,
} ;
downloads . unshift ( rec ) ;
dlItems . set ( id , item ) ;
emitDownloads ( ) ;
item . on ( "updated" , ( _ev , state ) => {
rec . state = state ; // "progressing" | "interrupted"
rec . received = item . getReceivedBytes ( ) ;
rec . total = item . getTotalBytes ( ) || rec . total ;
rec . savePath = item . getSavePath ( ) || rec . savePath ;
emitDownloads ( ) ;
} ) ;
item . once ( "done" , ( _ev , state ) => {
rec . state = state ; // "completed" | "cancelled" | "interrupted"
rec . received = item . getReceivedBytes ( ) ;
rec . savePath = item . getSavePath ( ) || rec . savePath ;
dlItems . delete ( id ) ;
emitDownloads ( ) ;
} ) ;
} ) ;
}
2026-07-29 13:54:34 +02:00
function setActive ( id ) {
activeId = id ;
2026-07-30 08:16:55 +02:00
if ( popVisible ) showPopover ( false ) ; // don't carry a stale popover across tabs
2026-07-30 22:55:52 +02:00
if ( epVisible ) showEnginePicker ( false ) ;
2026-07-29 13:54:34 +02:00
for ( const t of tabs ) t . view . setVisible ( t . id === id ) ;
const t = activeTab ( ) ;
if ( t ? . prov ) pushNav ( t . prov ) ;
chrome . webContents . send ( "bcnr-offer" , t ? . bcnrOffer ? { host : t . bcnrOffer . host , tld : t . bcnrOffer . tld , registry : REGISTRY } : null ) ;
emitTabs ( ) ;
}
function emitTabs ( ) {
const t = activeTab ( ) ;
const wc = t ? . view . webContents ;
chrome ? . webContents . send ( "tabs" , {
2026-07-30 19:29:32 +02:00
tabs : tabs . map ( ( x ) => ( { id : x . id , title : x . title || "New Tab" , active : x . id === activeId , loading : ! ! x . loading } ) ) ,
2026-07-29 13:54:34 +02:00
url : t ? . url || "" ,
2026-07-30 19:29:32 +02:00
loading : ! ! t ? . loading ,
2026-07-29 13:54:34 +02:00
canBack : wc ? wc . navigationHistory . canGoBack ( ) : false ,
canForward : wc ? wc . navigationHistory . canGoForward ( ) : false ,
} ) ;
}
2026-07-30 19:29:32 +02:00
function setLoading ( tab , on ) { if ( tab && tab . loading !== on ) { tab . loading = on ; emitTabs ( ) ; } }
2026-07-29 13:54:34 +02:00
function loadHome ( id ) {
const t = tabById ( id ) ; if ( ! t ) return ;
t . url = "" ; t . title = "Theseus" ; t . prov = { host : "" , kind : "home" } ;
t . view . webContents . loadFile ( "home.html" ) ;
if ( id === activeId ) pushNav ( t . prov ) ;
emitTabs ( ) ;
}
function createTab ( initial , opts = { } ) {
const id = ++ tabSeq ;
const view = new WebContentsView ( opts . settings ? { webPreferences : { preload : path . join ( _ _dirname , "settings-preload.js" ) } } : { } ) ;
const wc = view . webContents ;
try { wc . setWebRTCIPHandlingPolicy ( webrtcPolicy ( ) ) ; } catch { }
try { wc . setBackgroundThrottling ( settings . backgroundThrottle ) ; } catch { }
applyFingerprint ( wc ) ;
const tab = { id , view , title : opts . settings ? "Settings" : "New Tab" , url : "" , prov : null , settings : ! ! opts . settings } ;
tabs . push ( tab ) ;
win . contentView . addChildView ( view ) ;
wc . on ( "page-title-updated" , ( _e , title ) => { tab . title = title ; emitTabs ( ) ; } ) ;
wc . on ( "did-navigate" , ( ) => emitTabs ( ) ) ;
wc . on ( "did-navigate-in-page" , ( ) => emitTabs ( ) ) ;
2026-08-01 01:37:18 +02:00
wc . on ( "did-start-loading" , ( ) => setLoading ( tab , true ) ) ;
2026-07-30 19:29:32 +02:00
wc . on ( "did-stop-loading" , ( ) => setLoading ( tab , false ) ) ;
2026-07-29 13:54:34 +02:00
wc . on ( "will-navigate" , ( e , u ) => {
try {
const parsed = new URL ( u ) ;
if ( parsed . protocol === "bns:" ) return ;
if ( isBnsHost ( parsed . hostname ) ) { e . preventDefault ( ) ; navigateTab ( id , parsed . hostname + parsed . pathname ) ; }
} catch { }
} ) ;
// Links that open a new tab: target="_blank", window.open, Ctrl/middle-click.
wc . setWindowOpenHandler ( ( { url , disposition } ) => {
if ( url && url !== "about:blank" ) createTab ( url , { background : disposition === "background-tab" } ) ;
return { action : "deny" } ;
} ) ;
// Right-click context menu.
wc . on ( "context-menu" , ( _e , p ) => {
const items = [ ] ;
if ( p . linkURL ) {
items . push (
{ label : "Open link in new tab" , click : ( ) => createTab ( p . linkURL ) } ,
{ label : "Open link in new background tab" , click : ( ) => createTab ( p . linkURL , { background : true } ) } ,
{ label : "Copy link address" , click : ( ) => clipboard . writeText ( p . linkURL ) } ,
{ type : "separator" } ,
) ;
}
if ( p . isEditable ) items . push ( { role : "cut" } , { role : "copy" } , { role : "paste" } , { type : "separator" } ) ;
else if ( p . selectionText ) items . push ( { role : "copy" } , { type : "separator" } ) ;
items . push (
{ label : "Back" , enabled : wc . navigationHistory . canGoBack ( ) , click : ( ) => wc . navigationHistory . goBack ( ) } ,
{ label : "Forward" , enabled : wc . navigationHistory . canGoForward ( ) , click : ( ) => wc . navigationHistory . goForward ( ) } ,
{ label : "Reload" , click : ( ) => wc . reload ( ) } ,
) ;
Menu . buildFromTemplate ( items ) . popup ( ) ;
} ) ;
layout ( ) ;
if ( opts . background ) { view . setVisible ( false ) ; emitTabs ( ) ; }
else setActive ( id ) ;
if ( opts . settings ) {
tab . prov = { host : "" , kind : "home" } ;
wc . loadFile ( "settings.html" ) ;
if ( id === activeId ) pushNav ( tab . prov ) ;
emitTabs ( ) ;
} else if ( initial ) navigateTab ( id , initial ) ;
else loadHome ( id ) ;
return id ;
}
function closeTab ( id ) {
const i = tabs . findIndex ( ( t ) => t . id === id ) ;
if ( i < 0 ) return ;
const [ t ] = tabs . splice ( i , 1 ) ;
win . contentView . removeChildView ( t . view ) ;
t . view . webContents . destroy ? . ( ) ;
if ( tabs . length === 0 ) { createTab ( ) ; return ; }
if ( activeId === id ) setActive ( tabs [ Math . max ( 0 , i - 1 ) ] . id ) ;
else emitTabs ( ) ;
}
function createWindow ( ) {
win = new BrowserWindow ( { width : 1220 , height : 840 , title : "Theseus Navigator" , backgroundColor : "#0f1420" } ) ;
chrome = new WebContentsView ( { webPreferences : { preload : path . join ( _ _dirname , "preload.js" ) } } ) ;
win . contentView . addChildView ( chrome ) ;
chrome . webContents . loadFile ( "chrome.html" ) ;
2026-07-30 08:16:55 +02:00
// Floating site-info overlay (hidden until the address-bar badge is clicked).
popover = new WebContentsView ( { webPreferences : { preload : path . join ( _ _dirname , "popover-preload.js" ) } } ) ;
try { popover . setBackgroundColor ( "#00000000" ) ; } catch { }
win . contentView . addChildView ( popover ) ;
popover . webContents . loadFile ( "popover.html" ) ;
popover . setVisible ( false ) ;
2026-07-30 22:55:52 +02:00
// Floating engine-picker overlay (custom dropdown with real favicons).
enginePicker = new WebContentsView ( { webPreferences : { preload : path . join ( _ _dirname , "engine-picker-preload.js" ) } } ) ;
try { enginePicker . setBackgroundColor ( "#00000000" ) ; } catch { }
win . contentView . addChildView ( enginePicker ) ;
enginePicker . webContents . loadFile ( "engine-picker.html" ) ;
enginePicker . setVisible ( false ) ;
2026-08-02 11:39:00 +02:00
// Floating downloads panel — shows active + recent downloads.
downloadsPop = new WebContentsView ( { webPreferences : { preload : path . join ( _ _dirname , "downloads-preload.js" ) } } ) ;
try { downloadsPop . setBackgroundColor ( "#00000000" ) ; } catch { }
win . contentView . addChildView ( downloadsPop ) ;
downloadsPop . webContents . loadFile ( "downloads.html" ) ;
downloadsPop . setVisible ( false ) ;
2026-07-29 13:54:34 +02:00
chrome . webContents . once ( "did-finish-load" , ( ) => {
const saved = settings . restoreSession ? loadSession ( ) : [ ] ;
if ( saved . length ) saved . forEach ( ( u ) => createTab ( u ) ) ; else createTab ( ) ;
} ) ;
win . on ( "resize" , layout ) ;
layout ( ) ;
}
async function navigateTab ( id , input ) {
const t = tabById ( id ) ; if ( ! t ) return ;
let q = String ( input ) . trim ( ) ;
if ( ! q ) return ;
2026-07-30 19:29:32 +02:00
setLoading ( t , true ) ; // show the loading indicator immediately (covers BNS resolution)
2026-07-29 13:54:34 +02:00
// Address bar doubles as a search box: anything that isn't a URL/hostname
// (a bare word, or a phrase with spaces) becomes a web search.
if ( ! looksLikeUrl ( q ) ) q = SEARCH ( q ) ;
const raw = q . replace ( /^[a-z]+:\/\//i , "" ) ;
const host = raw . split ( "/" ) [ 0 ] . toLowerCase ( ) ;
const rest = raw . slice ( host . length ) || "/" ;
t . nav = ( t . nav || 0 ) + 1 ;
2026-07-30 22:55:52 +02:00
// Legacy "also on BCNR" chip state — kept clean; the passive switch is gone
// now that BCNR is priority for every host.
2026-07-29 13:54:34 +02:00
t . bcnrOffer = null ;
if ( id === activeId ) chrome . webContents . send ( "bcnr-offer" , null ) ;
2026-07-30 22:55:52 +02:00
// BCNR-first for every dotted host. loadBns falls through to https://<host>
// on NXDOMAIN or resolver failure, so clearnet still works.
if ( isBnsHost ( host ) ) return loadBns ( t , id , host , rest , tldOf ( host ) ) ;
2026-07-29 13:54:34 +02:00
2026-07-30 22:55:52 +02:00
// Non-BNS-eligible input: raw IP, localhost, single-label — load direct.
2026-07-29 13:54:34 +02:00
t . url = q . includes ( "://" ) ? q : "https://" + q ;
await t . view . webContents . loadURL ( t . url ) ;
t . prov = { host , kind : "web" } ;
if ( id === activeId ) pushNav ( t . prov ) ;
emitTabs ( ) ;
}
2026-07-30 22:55:52 +02:00
// Load a name from BCNR into a tab. Called for every dotted host — BCNR is
// tried first; on NXDOMAIN or resolver failure we always fall through to the
// clearnet (https://<host><rest>) so the user isn't stranded when the chain
// is down or the name isn't registered.
2026-07-29 13:54:34 +02:00
async function loadBns ( t , id , host , rest , tld ) {
const registry = registryOf ( tld ) ;
if ( id === activeId ) pushNav ( { host , kind : "resolving" , tld , registry } ) ;
2026-07-30 22:09:26 +02:00
const fallbackToWeb = async ( reason ) => {
t . url = "https://" + host + ( rest === "/" ? "" : rest ) ;
try { await t . view . webContents . loadURL ( t . url ) ; } catch { }
t . prov = { host , kind : "web" , note : ` fallback: ${ reason } ` , tld , registry } ;
if ( id === activeId ) pushNav ( t . prov ) ;
emitTabs ( ) ;
} ;
2026-08-02 14:43:31 +02:00
// Strip and capture the one-shot ?_collision=bcnr param that collision-choose
// adds when redirecting back after the user picked BCDN in soft mode. Ignored
// (harmless) if absent.
let urlChoice = null ;
try {
const u = new URL ( rest , ` bns:// ${ host } / ` ) ;
if ( u . searchParams . has ( "_collision" ) ) {
urlChoice = u . searchParams . get ( "_collision" ) ;
u . searchParams . delete ( "_collision" ) ;
rest = u . pathname + ( u . search ? u . search : "" ) ;
}
} catch { }
2026-07-29 13:54:34 +02:00
let entry ;
try { entry = await resolveHost ( host ) ; }
2026-07-30 22:55:52 +02:00
catch { setLoading ( t , false ) ; return fallbackToWeb ( "BCNR unreachable" ) ; }
2026-07-29 13:54:34 +02:00
t . url = host + ( rest === "/" ? "" : rest ) ;
2026-07-30 22:55:52 +02:00
if ( ! entry ) { setLoading ( t , false ) ; return fallbackToWeb ( "no BCNR record" ) ; }
2026-08-02 11:39:00 +02:00
// ---- Collision policy (BCNR ↔ ICANN) --------------------------------------
// BCNR has the name; if the TLD is BCNR-native we're done (whole TLD is BCNR's,
// no collision possible). Otherwise it's a collision candidate — the same name
// *might* also exist on ICANN; the policy decides which to load.
// Full model: SilentMode/Argus/DESIGN-collision-modes.md.
if ( ! isBcnrNativeTld ( tld ) ) {
2026-08-02 14:43:31 +02:00
// Transient per-tab override (from the chip switcher) — one-shot, consumed here.
const transient = t . collisionOverride ; t . collisionOverride = null ;
2026-08-02 11:39:00 +02:00
const policy = settings . collisionPolicy || "bcnr-first" ;
2026-08-02 14:43:31 +02:00
// Precedence: urlChoice (one-shot from collision-choose) > transient (chip
// switcher) > persistent per-name/per-TLD > global policy.
let choice = urlChoice || transient || overrideFor ( host , tld ) ;
2026-08-02 11:39:00 +02:00
if ( ! choice ) {
if ( policy === "icann-first" ) choice = "icann" ;
else if ( policy === "soft" ) {
2026-08-02 14:43:31 +02:00
// In-tab full-page "Open with…" prompt (loaded from disk; buttons post
// back through bns://collision-choose/ which serveBns handles above).
setLoading ( t , false ) ;
const q = new URLSearchParams ( { host , tld , resturl : rest } ) . toString ( ) ;
await t . view . webContents . loadFile ( path . join ( _ _dirname , "collision.html" ) , { search : q } ) ;
t . prov = { host , kind : "resolving" , tld , registry } ;
if ( id === activeId ) pushNav ( t . prov ) ;
emitTabs ( ) ;
return ;
2026-08-02 11:39:00 +02:00
} else choice = "bcnr" ; // bcnr-first — the default
}
if ( choice === "icann" ) { setLoading ( t , false ) ; return fallbackToWeb ( "collision → ICANN" ) ; }
}
2026-07-29 13:54:34 +02:00
await t . view . webContents . loadURL ( ` bns:// ${ host } ${ rest } ` ) ;
const src = entry . records . h ? "on-chain (chain)" : entry . records . s3 ? "Sia network" : entry . records . ip ? "direct server" : entry . records . u ? "redirect" : "record" ;
t . prov = { host , kind : "ok" , source : src , category : entry . category , records : Object . keys ( entry . records ) , tld , registry } ;
if ( id === activeId ) pushNav ( t . prov ) ;
emitTabs ( ) ;
}
ipcMain . handle ( "navigate" , ( _e , input ) => navigateTab ( activeId , input ) ) ;
ipcMain . handle ( "search" , ( _e , q ) => navigateTab ( activeId , SEARCH ( q ) ) ) ;
ipcMain . handle ( "new-tab" , ( ) => createTab ( ) ) ;
ipcMain . handle ( "close-tab" , ( _e , id ) => closeTab ( id ) ) ;
ipcMain . handle ( "switch-tab" , ( _e , id ) => setActive ( id ) ) ;
ipcMain . handle ( "go-home" , ( ) => loadHome ( activeId ) ) ;
ipcMain . handle ( "go-back" , ( ) => { const wc = activeTab ( ) ? . view . webContents ; if ( wc ? . navigationHistory . canGoBack ( ) ) wc . navigationHistory . goBack ( ) ; } ) ;
ipcMain . handle ( "go-forward" , ( ) => { const wc = activeTab ( ) ? . view . webContents ; if ( wc ? . navigationHistory . canGoForward ( ) ) wc . navigationHistory . goForward ( ) ; } ) ;
ipcMain . handle ( "reload" , ( ) => activeTab ( ) ? . view . webContents . reload ( ) ) ;
2026-07-30 19:29:32 +02:00
ipcMain . handle ( "stop" , ( ) => { const t = activeTab ( ) ; try { t ? . view . webContents . stop ( ) ; } catch { } setLoading ( t , false ) ; } ) ;
2026-07-29 13:54:34 +02:00
ipcMain . handle ( "toggle-tor" , ( ) => { torState === "off" ? startTor ( ) : stopTor ( ) ; } ) ;
ipcMain . handle ( "open-settings" , ( ) => { const ex = tabs . find ( ( t ) => t . settings ) ; if ( ex ) return setActive ( ex . id ) ; createTab ( null , { settings : true } ) ; } ) ;
2026-07-30 08:16:55 +02:00
ipcMain . handle ( "toggle-site-info" , ( _e , rect ) => {
if ( popVisible ) return showPopover ( false ) ;
if ( rect ) popPos = { x : Math . round ( rect . x ) , y : Math . round ( rect . y ) } ;
showPopover ( true ) ;
} ) ;
ipcMain . handle ( "close-site-info" , ( ) => showPopover ( false ) ) ;
2026-07-30 20:58:45 +02:00
ipcMain . handle ( "popover-resize" , ( _e , h ) => { popH = Math . max ( 90 , Math . min ( 380 , Math . round ( h ) || 210 ) ) ; if ( popVisible ) positionPopover ( ) ; } ) ;
2026-08-02 11:39:00 +02:00
// ---- Collision-mode: per-tab live switcher + policy control -----------------
// Flip the active tab between BCNR and ICANN for its current host, optionally
// remembering the choice per-name / per-TLD (like the OS "Open with…" flow).
ipcMain . handle ( "collision-switch" , async ( _e , arg ) => {
const t = activeTab ( ) ; if ( ! t ? . prov ? . host ) return null ;
const host = t . prov . host , tld = tldOf ( host ) ;
2026-08-02 14:43:31 +02:00
// Accept both "bcdn" (client-facing product name) and "bcnr" (internal key).
const raw = arg ? . choice ;
const choice = ( raw === "bcdn" || raw === "bcnr" ) ? "bcnr"
: ( raw === "icann" ) ? "icann"
: ( t . prov . kind === "ok" ? "icann" : "bcnr" ) ; // flip the current one
// Transient per-tab override consumed once by loadBns — no soft-mode re-prompt
// for this specific chip-triggered switch.
t . collisionOverride = choice ;
// Optional persistent memory ("Always use…" from the popover switcher).
2026-08-02 11:39:00 +02:00
rememberCollision ( host , tld , choice , arg ? . remember || "no" ) ;
return navigateTab ( t . id , host + ( t . prov . path || "/" ) ) ;
} ) ;
ipcMain . handle ( "collision-state" , ( ) => ( {
policy : settings . collisionPolicy ,
byName : collisions . byName ,
byTld : collisions . byTld ,
bcnrTlds ,
} ) ) ;
ipcMain . handle ( "collision-set-policy" , ( _e , p ) => {
if ( [ "bcnr-first" , "icann-first" , "soft" ] . includes ( p ) ) { settings . collisionPolicy = p ; saveSettings ( ) ; }
return settings . collisionPolicy ;
} ) ;
ipcMain . handle ( "collision-reset" , ( ) => { collisions = { byName : { } , byTld : { } } ; saveCollisions ( ) ; return true ; } ) ;
2026-07-30 20:58:45 +02:00
ipcMain . handle ( "search-engines" , ( ) => ( { engines : allEngines ( ) , current : settings . searchEngine } ) ) ;
2026-07-30 08:16:55 +02:00
ipcMain . handle ( "set-search-engine" , ( _e , id ) => {
2026-07-30 20:58:45 +02:00
if ( allEngines ( ) . some ( ( e ) => e . id === id ) ) { settings . searchEngine = id ; saveSettings ( ) ; emitEngines ( ) ; }
2026-07-30 08:16:55 +02:00
return settings . searchEngine ;
} ) ;
2026-07-30 20:58:45 +02:00
ipcMain . handle ( "add-engine" , ( _e , eng ) => {
// A custom engine needs a name and a URL template containing "%s".
if ( eng && eng . name && eng . url && String ( eng . url ) . includes ( "%s" ) ) {
const id = "custom-" + Date . now ( ) . toString ( 36 ) ;
settings . customEngines = [ ... ( settings . customEngines || [ ] ) ,
2026-07-30 22:09:26 +02:00
{ id , name : String ( eng . name ) . slice ( 0 , 40 ) , sym : String ( eng . sym || "🔍" ) . slice ( 0 , 4 ) , url : String ( eng . url ) . slice ( 0 , 400 ) } ] ;
2026-07-30 20:58:45 +02:00
settings . searchEngine = id ; // select the one just added
saveSettings ( ) ; emitEngines ( ) ;
}
return { engines : allEngines ( ) , current : settings . searchEngine } ;
} ) ;
ipcMain . handle ( "remove-engine" , ( _e , id ) => {
settings . customEngines = ( settings . customEngines || [ ] ) . filter ( ( e ) => e . id !== id ) ;
2026-07-30 22:55:52 +02:00
if ( settings . searchEngine === id ) settings . searchEngine = enabledEnginesList ( ) [ 0 ] ? . id || "duckduckgo" ;
2026-07-30 20:58:45 +02:00
saveSettings ( ) ; emitEngines ( ) ;
return { engines : allEngines ( ) , current : settings . searchEngine } ;
} ) ;
2026-07-30 22:55:52 +02:00
// Enable/disable a built-in engine (which show in the toolbar dropdown).
ipcMain . handle ( "set-engine-enabled" , ( _e , id , on ) => {
if ( SEARCH _ENGINES [ id ] ) {
let list = ( settings . enabledEngines || DEFAULT _ENABLED ) . filter ( ( x ) => x !== id ) ;
if ( on ) list . push ( id ) ;
settings . enabledEngines = list . length ? list : [ "duckduckgo" ] ; // never empty
if ( ! enabledEnginesList ( ) . some ( ( e ) => e . id === settings . searchEngine ) )
settings . searchEngine = enabledEnginesList ( ) [ 0 ] ? . id || "duckduckgo" ;
saveSettings ( ) ; emitEngines ( ) ;
}
return { engines : allEngines ( ) , current : settings . searchEngine } ;
} ) ;
2026-07-30 23:26:00 +02:00
ipcMain . handle ( "set-engine-order" , ( _e , ids ) => {
if ( Array . isArray ( ids ) ) { settings . engineOrder = ids . filter ( ( x ) => typeof x === "string" ) ; saveSettings ( ) ; emitEngines ( ) ; }
return { engines : allEngines ( ) , current : settings . searchEngine } ;
} ) ;
2026-07-30 22:55:52 +02:00
// The custom dropdown overlay (real favicons).
ipcMain . handle ( "toggle-engine-picker" , ( _e , rect ) => {
if ( epVisible ) return showEnginePicker ( false ) ;
if ( rect ) epPos = { x : Math . round ( rect . x ) , y : Math . round ( rect . y ) } ;
showEnginePicker ( true ) ;
} ) ;
ipcMain . handle ( "close-engine-picker" , ( ) => showEnginePicker ( false ) ) ;
ipcMain . handle ( "ep-resize" , ( _e , h ) => { epH = Math . max ( 80 , Math . min ( 440 , Math . round ( h ) || 320 ) ) ; if ( epVisible ) positionEnginePicker ( ) ; } ) ;
ipcMain . handle ( "pick-engine" , ( _e , id ) => {
if ( enabledEnginesList ( ) . some ( ( e ) => e . id === id ) ) { settings . searchEngine = id ; saveSettings ( ) ; emitEngines ( ) ; }
showEnginePicker ( false ) ;
} ) ;
ipcMain . handle ( "picker-open-settings" , ( ) => { showEnginePicker ( false ) ; const ex = tabs . find ( ( t ) => t . settings ) ; if ( ex ) return setActive ( ex . id ) ; createTab ( null , { settings : true } ) ; } ) ;
2026-08-02 11:39:00 +02:00
// ---- Downloads --------------------------------------------------------------
ipcMain . handle ( "downloads-get" , ( ) => downloadsPublic ( ) ) ;
ipcMain . handle ( "toggle-downloads" , ( _e , rect ) => {
if ( dlVisible ) return showDownloads ( false ) ;
if ( rect ) dlPos = { x : Math . round ( rect . x ) , y : Math . round ( rect . y ) } ;
showDownloads ( true ) ;
} ) ;
ipcMain . handle ( "close-downloads" , ( ) => showDownloads ( false ) ) ;
ipcMain . handle ( "downloads-resize" , ( _e , h ) => { dlH = Math . max ( 80 , Math . min ( 480 , Math . round ( h ) || 240 ) ) ; if ( dlVisible ) positionDownloads ( ) ; } ) ;
ipcMain . handle ( "download-open" , ( _e , id ) => {
const d = downloads . find ( ( x ) => x . id === id ) ;
if ( d ? . state === "completed" && d . savePath ) shell . openPath ( d . savePath ) . catch ( ( ) => { } ) ;
} ) ;
ipcMain . handle ( "download-show" , ( _e , id ) => {
const d = downloads . find ( ( x ) => x . id === id ) ;
if ( d ? . savePath ) { try { shell . showItemInFolder ( d . savePath ) ; } catch { } }
} ) ;
ipcMain . handle ( "download-cancel" , ( _e , id ) => {
const item = dlItems . get ( id ) ; if ( item ) { try { item . cancel ( ) ; } catch { } }
} ) ;
// Only clear finished downloads; a progressing one is cancelled first.
ipcMain . handle ( "download-clear" , ( _e , id ) => {
const i = downloads . findIndex ( ( x ) => x . id === id ) ;
if ( i < 0 ) return ;
if ( downloads [ i ] . state === "progressing" ) { const item = dlItems . get ( id ) ; if ( item ) { try { item . cancel ( ) ; } catch { } } }
downloads . splice ( i , 1 ) ; dlItems . delete ( id ) ;
emitDownloads ( ) ;
} ) ;
ipcMain . handle ( "downloads-clear-all" , ( ) => {
// Keep any still-progressing ones; drop everything else.
for ( let i = downloads . length - 1 ; i >= 0 ; i -- ) if ( downloads [ i ] . state !== "progressing" ) downloads . splice ( i , 1 ) ;
emitDownloads ( ) ;
} ) ;
2026-07-30 22:55:52 +02:00
// OpenSearch "scan": add the search engine the current page advertises.
ipcMain . handle ( "add-detected-engine" , ( ) => {
const d = activeTab ( ) ? . detected ;
if ( d && d . url && d . url . includes ( "%s" ) ) {
const id = "custom-" + Date . now ( ) . toString ( 36 ) ;
settings . customEngines = [ ... ( settings . customEngines || [ ] ) , { id , name : d . name . slice ( 0 , 40 ) , sym : "🔍" , url : d . url . slice ( 0 , 400 ) } ] ;
settings . searchEngine = id ;
saveSettings ( ) ; emitEngines ( ) ;
}
showEnginePicker ( false ) ;
} ) ;
2026-07-30 08:16:55 +02:00
ipcMain . handle ( "bookmarks-get" , ( ) => bookmarks ) ;
ipcMain . handle ( "bookmark-add" , ( _e , bm ) => {
if ( bm && bm . url && ! bookmarks . some ( ( b ) => b . url === bm . url ) ) {
bookmarks . push ( { title : bm . title || bm . url , url : bm . url } ) ;
saveBookmarks ( ) ; emitBookmarks ( ) ;
}
return bookmarks ;
} ) ;
ipcMain . handle ( "bookmark-remove" , ( _e , url ) => {
bookmarks = bookmarks . filter ( ( b ) => b . url !== url ) ;
saveBookmarks ( ) ; emitBookmarks ( ) ;
return bookmarks ;
} ) ;
2026-07-29 13:54:34 +02:00
ipcMain . handle ( "settings-get" , ( ) => settings ) ;
ipcMain . handle ( "settings-set" , ( _e , key , val ) => {
if ( key in SETTINGS _DEFAULTS ) { settings [ key ] = val ; saveSettings ( ) ; }
2026-07-30 22:00:01 +02:00
if ( key === "webrtcMode" ) applyWebRTCPolicy ( ) ;
if ( key === "theme" ) applyTheme ( ) ;
2026-07-29 13:54:34 +02:00
if ( key === "backgroundThrottle" ) applyThrottle ( ) ;
2026-07-30 23:11:28 +02:00
if ( [ "timezoneMode" , "timezoneValue" , "languageMode" , "languageSpoof" , "languageValue" ,
"locationMode" , "locationRegion" , "locationLat" , "locationLon" , "hideMediaDevices" ] . includes ( key ) ) { applyFingerprintAll ( ) ; applyAcceptLanguage ( ) ; }
2026-07-29 13:54:34 +02:00
return settings ;
} ) ;
ipcMain . handle ( "set-chrome-height" , ( _e , h ) => {
const next = Math . max ( 74 , Math . min ( 260 , Math . round ( h ) || 84 ) ) ;
if ( next !== CHROME _H ) { CHROME _H = next ; layout ( ) ; }
} ) ;
ipcMain . handle ( "switch-to-bcnr" , ( ) => {
const t = activeTab ( ) ; if ( ! t || ! t . bcnrOffer ) return ;
const { host , rest , tld } = t . bcnrOffer ;
t . bcnrOffer = null ;
chrome . webContents . send ( "bcnr-offer" , null ) ;
return loadBns ( t , activeId , host , rest || "/" , tld ) ;
} ) ;
// THESEUS_NO_AUTOSTART lets a test harness reuse serveBns/resolveHost without
// launching the full UI (see dev/selftest.js). Normal `npm start` is unchanged.
if ( ! process . env . THESEUS _NO _AUTOSTART ) {
app . whenReady ( ) . then ( ( ) => {
2026-07-30 08:16:55 +02:00
Menu . setApplicationMenu ( null ) ; // drop the native File/Edit/View/Help menu bar
2026-07-29 13:54:34 +02:00
loadSettings ( ) ;
2026-07-30 22:00:01 +02:00
applyTheme ( ) ;
2026-07-30 08:16:55 +02:00
loadBookmarks ( ) ;
2026-08-02 11:39:00 +02:00
loadCollisions ( ) ;
2026-07-29 13:54:34 +02:00
applyPermissions ( ) ;
applyAcceptLanguage ( ) ;
protocol . handle ( "bns" , serveBns ) ;
2026-08-02 11:39:00 +02:00
installDownloadTracker ( ) ;
2026-07-29 13:54:34 +02:00
createWindow ( ) ;
2026-07-30 19:29:32 +02:00
ensureIndex ( ) . catch ( ( ) => { } ) ; // warm the chain index so the first .bch load is fast
2026-07-29 13:54:34 +02:00
app . on ( "activate" , ( ) => { if ( BrowserWindow . getAllWindows ( ) . length === 0 ) createWindow ( ) ; } ) ;
} ) ;
app . on ( "before-quit" , ( ) => { saveSession ( ) ; stopTor ( ) ; } ) ;
app . on ( "window-all-closed" , ( ) => { stopTor ( ) ; if ( process . platform !== "darwin" ) app . quit ( ) ; } ) ;
}
module . exports = { serveBns , resolveHost , isBnsHost , nativeTld , dualTld , registryOf } ;