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 18:50:58 +02:00
const { app , BrowserWindow , WebContentsView , ipcMain , protocol , session , Menu , clipboard , nativeTheme , shell , dialog } = require ( "electron" ) ;
2026-07-29 13:54:34 +02:00
const path = require ( "path" ) ;
const http = require ( "http" ) ;
const https = require ( "https" ) ;
gateway+Theseus: pin BNS ip-record fetch to on-chain tls fingerprint
Uncovered by the 2026-08-13 subdomain-inheritance fix: once
`checkers.game.x` correctly picked the parent's `ip` record instead of
`s3`, the ip branch itself failed. Two reasons:
1. `fetch("http://<ip>/", { headers: { host: name } })` follows the
site's :80→:443 redirect into `https://<name>.<tld>/`, which isn't
in ICANN DNS → "fetch failed".
2. The site's cert is signed by a per-machine BNS root, not a public
CA; standard TLS validation rejects it.
Both are fixed by connecting to the IP with SNI = name, pinning the
presented cert's SHA-256 against the on-chain `tls` record, and only
then issuing the HTTPS request over the same socket. The on-chain
fingerprint is the trust anchor BNS uses everywhere else (see
Argus/src/lib/ca.js).
Gateway (public-gateway.mjs): new pinnedHttpsGet + httpGet + ipRequest
helpers; case "ip" delegates. No silent HTTP fallback on pin failure
(a mismatch means "not the site the chain says it is").
Theseus (main.js): parallel port of the same helpers, Tor-aware
(routes through SocksProxyAgent when Tor is on). serveBns's inner
serveIp() delegates to ipRequest.
Verified live: `curl -sI https://navigate.st/bns/checkers.game.x/`
returns 200 OK with the checkers game (1,179,215 bytes, apex
`game.x` unchanged, served from Sia).
2026-08-16 20:28:58 +02:00
const tls = require ( "tls" ) ;
2026-07-29 13:54:34 +02:00
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" ) ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
// 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: wire NIP-17 messaging into Theseus, provision chipnet hermes.bch
- Hermes/proof/: standalone keystone — BIP-39→Nostr, NIP-17 wrap/unwrap,
name-addressed send/receive verified end-to-end via public relay
- Argus/src/lib/hermes-derive.js: HKDF(silentmode/messenger/0) using libauth
so the registrar can compute np without pulling nostr-tools into Argus
- Argus/src/register-hermes-chipnet.mjs: idempotent REG/UPD script; publishes
np (Nostr pubkey) and nr (relay) records for a chipnet name
- TheseusNavigator/lib/hermes.js: same derivation + wrap/unwrap + record
parsing, canonical for Theseus; guarded by lib/package.json type:module
- TheseusNavigator/messages.html + messages-preload.js: Messages panel UI
(identity from mnemonic, live inbox, compose by .bch name), .bch suffix
stripped from displayed names
- TheseusNavigator/main.js: HERMES_MOD + loadHermesLib next to VAULT_MOD;
ipc handlers (hermes-status/init/close/inbox/send/open), per-relay
subscription with auto-reconnect, status pushed on WS open/close,
reverse-resolve pk -> .bch name via cached BNS index, Ctrl+Shift+M shortcut
via web-contents-created (works from any tab)
- Chipnet hermes.bch registered with np=f2c92519...67f4 nr=wss://nos.lol
(txid 7fc6de4544c13b782f163fdb892ea6749785886d05a336282fa659d722c7e92b);
end-to-end verified in Theseus
2026-08-18 20:14:43 +02:00
// 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 ;
}
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).
Theseus: download tracker, search split, discover-more tier, UX polish
Bundle of UX + feature work. Split from packaging by intent so the diff
is reviewable; the next Theseus rebuild ships it.
Features
- Download tracker (new): session.on("will-download") → per-item state
{id, filename, url, mime, total, received, state, savePath, startedAt}
with updated/done event handlers. New downloadsPop WebContentsView
loads downloads.html (new file) + downloads-preload.js (new file);
panel positioned under a new #downloads toolbar button between search
and Tor. Full IPC: downloads-get, toggle/close/resize-downloads,
download-open/show/cancel/clear, downloads-clear-all. In-memory only —
cross-session persistence is a future addition. Button badge shows
active count + spin/done/err color.
- Search engines split by kind + tier:
* kind: "search" | "llm" — separate headers in picker + settings
("Search with" / "Ask an AI"). Empty sections hidden.
* tier: "catalog" | "extra" — Settings now has THREE panes behind the
"+ Add search engine" button: curated catalog, wider discoverable
bank filtered by a live search input, custom URL form.
* DEFAULT_ENABLED unchanged (5 major engines).
* Custom user-added engines carry tier="custom" (never in catalog/extra
panes).
- 9 tier="extra" engines added (all non-login ?q=): Marginalia, Stract,
Yep, Presearch, MetaGer, Qwant, Swisscows, Naver, Baidu. Same rot rule
as LLMs: if one starts bouncing to a login gate, drop it.
Bug fixes
- Loadbar collapses to 0px when idle (was reserving a permanent 2px
strip below the address bar). .loadbar {height:0} + .loadbar.on
{height:2px} + 120ms transition.
- Native <select> popup theme sync via :root { color-scheme: dark } +
@media(prefers-color-scheme: light). nativeTheme.themeSource already
drives prefers-color-scheme, so the OS popup color follows the app
theme automatically (fixed light popup on dark app / vice versa).
- .ctl layout flipped to flex-direction: row with flex-wrap so
anti-fingerprint mode + value fields fit side-by-side.
Settings restructure
- General section: Startup group at the top ("Open previous windows and
tabs" toggle), then Appearance below with three visual THEME CARDS
(System / Light / Dark) — small mock-browser previews per theme,
Firefox-style, active card gets a blue ring. System pipes through to
nativeTheme.themeSource = "system".
- Search promoted to a top-level sidebar item between General and
Naming. Search-engine controls moved out of General into Search.
- Search section: enabled list shows only enabled engines, grouped by
kind, drag-reorder within a kind. "+ Add search engine" opens the
catalog/extras/custom-URL panel.
Search engine catalog trims (already flagged in prior work)
- Removed ChatGPT / Claude / You.com (login-gated ?q=).
- Removed SearXNG (federated; every single-instance default rots).
Docs
- TheseusNavigator/PENDING.md and GOTCHAS.md born with this work
(see the HANDOFF.md commit for the convention).
- PENDING.md's own "session: 2026-08-02:theseus-ux-polish" group will be
emptied after this ship lands.
Preview harness
- _preview.html + _settings-preview.html stubs updated with kind + tier
+ downloads seed + tier="extra" samples so the preview reflects reality.
Both files are gitignored — local only.
Coordination
- Parallel session's collision-policy work (chrome.html registry chips,
popover switcher, Naming section, in-tab collision prompt) already
landed in commits 256079d/42b340f/b0d6375/78dddda. This commit adds
cleanly on top.
2026-08-02 15:31:47 +02:00
// 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).
Theseus: download tracker, search split, discover-more tier, UX polish
Bundle of UX + feature work. Split from packaging by intent so the diff
is reviewable; the next Theseus rebuild ships it.
Features
- Download tracker (new): session.on("will-download") → per-item state
{id, filename, url, mime, total, received, state, savePath, startedAt}
with updated/done event handlers. New downloadsPop WebContentsView
loads downloads.html (new file) + downloads-preload.js (new file);
panel positioned under a new #downloads toolbar button between search
and Tor. Full IPC: downloads-get, toggle/close/resize-downloads,
download-open/show/cancel/clear, downloads-clear-all. In-memory only —
cross-session persistence is a future addition. Button badge shows
active count + spin/done/err color.
- Search engines split by kind + tier:
* kind: "search" | "llm" — separate headers in picker + settings
("Search with" / "Ask an AI"). Empty sections hidden.
* tier: "catalog" | "extra" — Settings now has THREE panes behind the
"+ Add search engine" button: curated catalog, wider discoverable
bank filtered by a live search input, custom URL form.
* DEFAULT_ENABLED unchanged (5 major engines).
* Custom user-added engines carry tier="custom" (never in catalog/extra
panes).
- 9 tier="extra" engines added (all non-login ?q=): Marginalia, Stract,
Yep, Presearch, MetaGer, Qwant, Swisscows, Naver, Baidu. Same rot rule
as LLMs: if one starts bouncing to a login gate, drop it.
Bug fixes
- Loadbar collapses to 0px when idle (was reserving a permanent 2px
strip below the address bar). .loadbar {height:0} + .loadbar.on
{height:2px} + 120ms transition.
- Native <select> popup theme sync via :root { color-scheme: dark } +
@media(prefers-color-scheme: light). nativeTheme.themeSource already
drives prefers-color-scheme, so the OS popup color follows the app
theme automatically (fixed light popup on dark app / vice versa).
- .ctl layout flipped to flex-direction: row with flex-wrap so
anti-fingerprint mode + value fields fit side-by-side.
Settings restructure
- General section: Startup group at the top ("Open previous windows and
tabs" toggle), then Appearance below with three visual THEME CARDS
(System / Light / Dark) — small mock-browser previews per theme,
Firefox-style, active card gets a blue ring. System pipes through to
nativeTheme.themeSource = "system".
- Search promoted to a top-level sidebar item between General and
Naming. Search-engine controls moved out of General into Search.
- Search section: enabled list shows only enabled engines, grouped by
kind, drag-reorder within a kind. "+ Add search engine" opens the
catalog/extras/custom-URL panel.
Search engine catalog trims (already flagged in prior work)
- Removed ChatGPT / Claude / You.com (login-gated ?q=).
- Removed SearXNG (federated; every single-instance default rots).
Docs
- TheseusNavigator/PENDING.md and GOTCHAS.md born with this work
(see the HANDOFF.md commit for the convention).
- PENDING.md's own "session: 2026-08-02:theseus-ux-polish" group will be
emptied after this ship lands.
Preview harness
- _preview.html + _settings-preview.html stubs updated with kind + tier
+ downloads seed + tier="extra" samples so the preview reflects reality.
Both files are gitignored — local only.
Coordination
- Parallel session's collision-policy work (chrome.html registry chips,
popover switcher, Naming section, in-tab collision prompt) already
landed in commits 256079d/42b340f/b0d6375/78dddda. This commit adds
cleanly on top.
2026-08-02 15:31:47 +02:00
// 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.
2026-07-30 08:16:55 +02:00
const SEARCH _ENGINES = {
Theseus: download tracker, search split, discover-more tier, UX polish
Bundle of UX + feature work. Split from packaging by intent so the diff
is reviewable; the next Theseus rebuild ships it.
Features
- Download tracker (new): session.on("will-download") → per-item state
{id, filename, url, mime, total, received, state, savePath, startedAt}
with updated/done event handlers. New downloadsPop WebContentsView
loads downloads.html (new file) + downloads-preload.js (new file);
panel positioned under a new #downloads toolbar button between search
and Tor. Full IPC: downloads-get, toggle/close/resize-downloads,
download-open/show/cancel/clear, downloads-clear-all. In-memory only —
cross-session persistence is a future addition. Button badge shows
active count + spin/done/err color.
- Search engines split by kind + tier:
* kind: "search" | "llm" — separate headers in picker + settings
("Search with" / "Ask an AI"). Empty sections hidden.
* tier: "catalog" | "extra" — Settings now has THREE panes behind the
"+ Add search engine" button: curated catalog, wider discoverable
bank filtered by a live search input, custom URL form.
* DEFAULT_ENABLED unchanged (5 major engines).
* Custom user-added engines carry tier="custom" (never in catalog/extra
panes).
- 9 tier="extra" engines added (all non-login ?q=): Marginalia, Stract,
Yep, Presearch, MetaGer, Qwant, Swisscows, Naver, Baidu. Same rot rule
as LLMs: if one starts bouncing to a login gate, drop it.
Bug fixes
- Loadbar collapses to 0px when idle (was reserving a permanent 2px
strip below the address bar). .loadbar {height:0} + .loadbar.on
{height:2px} + 120ms transition.
- Native <select> popup theme sync via :root { color-scheme: dark } +
@media(prefers-color-scheme: light). nativeTheme.themeSource already
drives prefers-color-scheme, so the OS popup color follows the app
theme automatically (fixed light popup on dark app / vice versa).
- .ctl layout flipped to flex-direction: row with flex-wrap so
anti-fingerprint mode + value fields fit side-by-side.
Settings restructure
- General section: Startup group at the top ("Open previous windows and
tabs" toggle), then Appearance below with three visual THEME CARDS
(System / Light / Dark) — small mock-browser previews per theme,
Firefox-style, active card gets a blue ring. System pipes through to
nativeTheme.themeSource = "system".
- Search promoted to a top-level sidebar item between General and
Naming. Search-engine controls moved out of General into Search.
- Search section: enabled list shows only enabled engines, grouped by
kind, drag-reorder within a kind. "+ Add search engine" opens the
catalog/extras/custom-URL panel.
Search engine catalog trims (already flagged in prior work)
- Removed ChatGPT / Claude / You.com (login-gated ?q=).
- Removed SearXNG (federated; every single-instance default rots).
Docs
- TheseusNavigator/PENDING.md and GOTCHAS.md born with this work
(see the HANDOFF.md commit for the convention).
- PENDING.md's own "session: 2026-08-02:theseus-ux-polish" group will be
emptied after this ship lands.
Preview harness
- _preview.html + _settings-preview.html stubs updated with kind + tier
+ downloads seed + tier="extra" samples so the preview reflects reality.
Both files are gitignored — local only.
Coordination
- Parallel session's collision-policy work (chrome.html registry chips,
popover switcher, Naming section, in-tab collision prompt) already
landed in commits 256079d/42b340f/b0d6375/78dddda. This commit adds
cleanly on top.
2026-08-02 15:31:47 +02:00
duckduckgo : { kind : "search" , tier : "catalog" , name : "DuckDuckGo" , sym : "🦆" , fav : "duckduckgo.com" , url : ( q ) => "https://duckduckgo.com/?q=" + encodeURIComponent ( q ) } ,
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
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 ` } ,
Theseus: download tracker, search split, discover-more tier, UX polish
Bundle of UX + feature work. Split from packaging by intent so the diff
is reviewable; the next Theseus rebuild ships it.
Features
- Download tracker (new): session.on("will-download") → per-item state
{id, filename, url, mime, total, received, state, savePath, startedAt}
with updated/done event handlers. New downloadsPop WebContentsView
loads downloads.html (new file) + downloads-preload.js (new file);
panel positioned under a new #downloads toolbar button between search
and Tor. Full IPC: downloads-get, toggle/close/resize-downloads,
download-open/show/cancel/clear, downloads-clear-all. In-memory only —
cross-session persistence is a future addition. Button badge shows
active count + spin/done/err color.
- Search engines split by kind + tier:
* kind: "search" | "llm" — separate headers in picker + settings
("Search with" / "Ask an AI"). Empty sections hidden.
* tier: "catalog" | "extra" — Settings now has THREE panes behind the
"+ Add search engine" button: curated catalog, wider discoverable
bank filtered by a live search input, custom URL form.
* DEFAULT_ENABLED unchanged (5 major engines).
* Custom user-added engines carry tier="custom" (never in catalog/extra
panes).
- 9 tier="extra" engines added (all non-login ?q=): Marginalia, Stract,
Yep, Presearch, MetaGer, Qwant, Swisscows, Naver, Baidu. Same rot rule
as LLMs: if one starts bouncing to a login gate, drop it.
Bug fixes
- Loadbar collapses to 0px when idle (was reserving a permanent 2px
strip below the address bar). .loadbar {height:0} + .loadbar.on
{height:2px} + 120ms transition.
- Native <select> popup theme sync via :root { color-scheme: dark } +
@media(prefers-color-scheme: light). nativeTheme.themeSource already
drives prefers-color-scheme, so the OS popup color follows the app
theme automatically (fixed light popup on dark app / vice versa).
- .ctl layout flipped to flex-direction: row with flex-wrap so
anti-fingerprint mode + value fields fit side-by-side.
Settings restructure
- General section: Startup group at the top ("Open previous windows and
tabs" toggle), then Appearance below with three visual THEME CARDS
(System / Light / Dark) — small mock-browser previews per theme,
Firefox-style, active card gets a blue ring. System pipes through to
nativeTheme.themeSource = "system".
- Search promoted to a top-level sidebar item between General and
Naming. Search-engine controls moved out of General into Search.
- Search section: enabled list shows only enabled engines, grouped by
kind, drag-reorder within a kind. "+ Add search engine" opens the
catalog/extras/custom-URL panel.
Search engine catalog trims (already flagged in prior work)
- Removed ChatGPT / Claude / You.com (login-gated ?q=).
- Removed SearXNG (federated; every single-instance default rots).
Docs
- TheseusNavigator/PENDING.md and GOTCHAS.md born with this work
(see the HANDOFF.md commit for the convention).
- PENDING.md's own "session: 2026-08-02:theseus-ux-polish" group will be
emptied after this ship lands.
Preview harness
- _preview.html + _settings-preview.html stubs updated with kind + tier
+ downloads seed + tier="extra" samples so the preview reflects reality.
Both files are gitignored — local only.
Coordination
- Parallel session's collision-policy work (chrome.html registry chips,
popover switcher, Naming section, in-tab collision prompt) already
landed in commits 256079d/42b340f/b0d6375/78dddda. This commit adds
cleanly on top.
2026-08-02 15:31:47 +02:00
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 ) } ,
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.
Theseus: download tracker, search split, discover-more tier, UX polish
Bundle of UX + feature work. Split from packaging by intent so the diff
is reviewable; the next Theseus rebuild ships it.
Features
- Download tracker (new): session.on("will-download") → per-item state
{id, filename, url, mime, total, received, state, savePath, startedAt}
with updated/done event handlers. New downloadsPop WebContentsView
loads downloads.html (new file) + downloads-preload.js (new file);
panel positioned under a new #downloads toolbar button between search
and Tor. Full IPC: downloads-get, toggle/close/resize-downloads,
download-open/show/cancel/clear, downloads-clear-all. In-memory only —
cross-session persistence is a future addition. Button badge shows
active count + spin/done/err color.
- Search engines split by kind + tier:
* kind: "search" | "llm" — separate headers in picker + settings
("Search with" / "Ask an AI"). Empty sections hidden.
* tier: "catalog" | "extra" — Settings now has THREE panes behind the
"+ Add search engine" button: curated catalog, wider discoverable
bank filtered by a live search input, custom URL form.
* DEFAULT_ENABLED unchanged (5 major engines).
* Custom user-added engines carry tier="custom" (never in catalog/extra
panes).
- 9 tier="extra" engines added (all non-login ?q=): Marginalia, Stract,
Yep, Presearch, MetaGer, Qwant, Swisscows, Naver, Baidu. Same rot rule
as LLMs: if one starts bouncing to a login gate, drop it.
Bug fixes
- Loadbar collapses to 0px when idle (was reserving a permanent 2px
strip below the address bar). .loadbar {height:0} + .loadbar.on
{height:2px} + 120ms transition.
- Native <select> popup theme sync via :root { color-scheme: dark } +
@media(prefers-color-scheme: light). nativeTheme.themeSource already
drives prefers-color-scheme, so the OS popup color follows the app
theme automatically (fixed light popup on dark app / vice versa).
- .ctl layout flipped to flex-direction: row with flex-wrap so
anti-fingerprint mode + value fields fit side-by-side.
Settings restructure
- General section: Startup group at the top ("Open previous windows and
tabs" toggle), then Appearance below with three visual THEME CARDS
(System / Light / Dark) — small mock-browser previews per theme,
Firefox-style, active card gets a blue ring. System pipes through to
nativeTheme.themeSource = "system".
- Search promoted to a top-level sidebar item between General and
Naming. Search-engine controls moved out of General into Search.
- Search section: enabled list shows only enabled engines, grouped by
kind, drag-reorder within a kind. "+ Add search engine" opens the
catalog/extras/custom-URL panel.
Search engine catalog trims (already flagged in prior work)
- Removed ChatGPT / Claude / You.com (login-gated ?q=).
- Removed SearXNG (federated; every single-instance default rots).
Docs
- TheseusNavigator/PENDING.md and GOTCHAS.md born with this work
(see the HANDOFF.md commit for the convention).
- PENDING.md's own "session: 2026-08-02:theseus-ux-polish" group will be
emptied after this ship lands.
Preview harness
- _preview.html + _settings-preview.html stubs updated with kind + tier
+ downloads seed + tier="extra" samples so the preview reflects reality.
Both files are gitignored — local only.
Coordination
- Parallel session's collision-policy work (chrome.html registry chips,
popover switcher, Naming section, in-tab collision prompt) already
landed in commits 256079d/42b340f/b0d6375/78dddda. This commit adds
cleanly on top.
2026-08-02 15:31:47 +02:00
wikipedia : { kind : "search" , tier : "catalog" , name : "Wikipedia" , sym : "📖" , fav : "en.wikipedia.org" , url : ( q ) => "https://en.wikipedia.org/wiki/Special:Search?search=" + encodeURIComponent ( q ) } ,
2026-08-02 11:39:00 +02:00
// 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.
Theseus: download tracker, search split, discover-more tier, UX polish
Bundle of UX + feature work. Split from packaging by intent so the diff
is reviewable; the next Theseus rebuild ships it.
Features
- Download tracker (new): session.on("will-download") → per-item state
{id, filename, url, mime, total, received, state, savePath, startedAt}
with updated/done event handlers. New downloadsPop WebContentsView
loads downloads.html (new file) + downloads-preload.js (new file);
panel positioned under a new #downloads toolbar button between search
and Tor. Full IPC: downloads-get, toggle/close/resize-downloads,
download-open/show/cancel/clear, downloads-clear-all. In-memory only —
cross-session persistence is a future addition. Button badge shows
active count + spin/done/err color.
- Search engines split by kind + tier:
* kind: "search" | "llm" — separate headers in picker + settings
("Search with" / "Ask an AI"). Empty sections hidden.
* tier: "catalog" | "extra" — Settings now has THREE panes behind the
"+ Add search engine" button: curated catalog, wider discoverable
bank filtered by a live search input, custom URL form.
* DEFAULT_ENABLED unchanged (5 major engines).
* Custom user-added engines carry tier="custom" (never in catalog/extra
panes).
- 9 tier="extra" engines added (all non-login ?q=): Marginalia, Stract,
Yep, Presearch, MetaGer, Qwant, Swisscows, Naver, Baidu. Same rot rule
as LLMs: if one starts bouncing to a login gate, drop it.
Bug fixes
- Loadbar collapses to 0px when idle (was reserving a permanent 2px
strip below the address bar). .loadbar {height:0} + .loadbar.on
{height:2px} + 120ms transition.
- Native <select> popup theme sync via :root { color-scheme: dark } +
@media(prefers-color-scheme: light). nativeTheme.themeSource already
drives prefers-color-scheme, so the OS popup color follows the app
theme automatically (fixed light popup on dark app / vice versa).
- .ctl layout flipped to flex-direction: row with flex-wrap so
anti-fingerprint mode + value fields fit side-by-side.
Settings restructure
- General section: Startup group at the top ("Open previous windows and
tabs" toggle), then Appearance below with three visual THEME CARDS
(System / Light / Dark) — small mock-browser previews per theme,
Firefox-style, active card gets a blue ring. System pipes through to
nativeTheme.themeSource = "system".
- Search promoted to a top-level sidebar item between General and
Naming. Search-engine controls moved out of General into Search.
- Search section: enabled list shows only enabled engines, grouped by
kind, drag-reorder within a kind. "+ Add search engine" opens the
catalog/extras/custom-URL panel.
Search engine catalog trims (already flagged in prior work)
- Removed ChatGPT / Claude / You.com (login-gated ?q=).
- Removed SearXNG (federated; every single-instance default rots).
Docs
- TheseusNavigator/PENDING.md and GOTCHAS.md born with this work
(see the HANDOFF.md commit for the convention).
- PENDING.md's own "session: 2026-08-02:theseus-ux-polish" group will be
emptied after this ship lands.
Preview harness
- _preview.html + _settings-preview.html stubs updated with kind + tier
+ downloads seed + tier="extra" samples so the preview reflects reality.
Both files are gitignored — local only.
Coordination
- Parallel session's collision-policy work (chrome.html registry chips,
popover switcher, Naming section, in-tab collision prompt) already
landed in commits 256079d/42b340f/b0d6375/78dddda. This commit adds
cleanly on top.
2026-08-02 15:31:47 +02:00
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 ) } ,
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.
2026-08-31 16:32:16 +02:00
const DEFAULT _ENABLED = [ "startpage" , "duckduckgo" , "google" , "brave" , "bing" ] ;
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 ) ; }
2026-08-06 01:41:31 +02:00
// 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 ) ;
}
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-06 01:41:31 +02:00
( { 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 ) } ) ) ;
2026-07-30 22:55:52 +02:00
for ( const c of settings . customEngines || [ ] )
2026-08-06 01:41:31 +02:00
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 ) } ) ;
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 ) ; }
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
// 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" ,
} ;
}
2026-07-30 20:58:45 +02:00
function engineUrl ( id , q ) {
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
const h = searchHints ( ) ;
if ( SEARCH _ENGINES [ id ] ) return SEARCH _ENGINES [ id ] . url ( q , h ) ;
2026-07-30 20:58:45 +02:00
const c = ( settings . customEngines || [ ] ) . find ( ( e ) => e . id === id ) ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
return c ? c . url . replace ( /%s/g , encodeURIComponent ( q ) ) : SEARCH _ENGINES . duckduckgo . url ( q , h ) ;
2026-07-30 20:58:45 +02:00
}
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
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
// Storage retention — nothing persists by default. Auto-clear on quit
// means a session leaves no trace on disk unless the user opts in per-type.
clearCookiesOnQuit : true , // drop cookies + logins + saved-form data
clearCacheOnQuit : true , // drop HTTP cache (images, scripts, etc.)
clearHistoryOnQuit : true , // drop navigation history + saved tabs
clearStorageOnQuit : true , // drop localStorage / IndexedDB / service workers / cache API
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-08-31 16:32:16 +02:00
searchEngine : "startpage" , // default search engine (built-in id or a custom id)
2026-08-06 01:41:31 +02:00
installedEngines : DEFAULT _ENABLED . slice ( ) , // built-in engines added to the user's list (visible in Settings)
enabledEngines : DEFAULT _ENABLED . slice ( ) , // subset that's currently toggled on (shown in the toolbar 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" ,
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
// Add-on framework: ids the user has explicitly turned off. Installed but
// disabled add-ons are still discovered — they just never activate.
disabledAddons : [ ] ,
2026-08-31 16:00:34 +02:00
// Sidebar width in px. Adjusted by dragging the grip on the panel's left
// edge; persisted across launches. Clamped to [200, 800] on load.
sidebarWidth : 340 ,
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 ) ; }
2026-08-31 16:00:34 +02:00
// Restore the persisted sidebar width so the first-open of a session
// uses whatever the user left it at last time.
const w = Number ( settings . sidebarWidth ) || SIDEBAR _W _DEFAULT ;
sidebarW = Math . max ( SIDEBAR _W _MIN , Math . min ( SIDEBAR _W _MAX , w ) ) ;
2026-07-29 13:54:34 +02:00
}
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
Theseus 0.0.4: cheap in-app update-check chip
Bumps version so the chip actually surfaces itself on 0.0.3 installs
(the version-newer check requires a strict semver bump — same-version
rebuilds don't trigger the chip). From this release on, whenever the
manifest names a newer Theseus, users get a one-click download.
Mechanism
- main.js checkForUpdate() fetches https://dl.silentmode.st/releases-
manifest.json on startup (5s timeout, cache: no-store) + every 6h.
Finds the theseus-navigator release, compares version to
app.getVersion() with a numeric a.b.c comparator that handles
"0.10.0 > 0.9.9" correctly.
- On a match → stores { version, setupUrl, portableUrl, setupHash,
portableHash, date } and emits update-available to chrome. Cleared
after the user upgrades + relaunches (same-version → null).
- Re-emits on chrome's did-finish-load in case the fetch beats the
chrome view.
Chip UI (chrome.html)
- Acid-yellow pill between the downloads button and the Tor toggle:
"↓ Update to X.Y.Z" + a ✕. Main body opens setupUrl in the system
browser via shell.openExternal (origin-validated to
https://dl.silentmode.st/ or https://silentmode.st/). ✕ dismisses
for the current session — you'll see it again next launch if still
behind.
Trust anchor
- No signing / no cryptographic verification of the download in this
phase. releases.silentmode.bch publishes the SAME manifest URL, so
users who want to verify can cross-check the manifest hash against
what BCNR returns. The proper auto-updater with signature checks is
the follow-on to this cheap version.
Non-goals in phase 1
- No delta downloads; the user clicks and gets a full installer.
- No auto-install; download → user runs the installer themselves.
- No "check now" button in Settings; the periodic timer suffices.
- No portable-vs-installed detection; the chip prefers setupUrl (the
installer upgrades in place). Right-click for portable is future work.
2026-08-28 20:21:44 +02:00
// ---- in-app update check (cheap) ------------------------------------------
// Fetch the releases manifest at startup + every 6h. If it names a Theseus
// version newer than ours, surface a chip in the toolbar with a link to the
// download URL. No auto-install, no signing check — the on-chain pointer at
// releases.silentmode.bch publishes the SAME manifest URL, so users who want
// to verify integrity can compare the manifest hash to what BCNR returns.
const UPDATE _MANIFEST _URL = "https://dl.silentmode.st/releases-manifest.json" ;
const UPDATE _DOWNLOAD _BASE = "https://dl.silentmode.st/" ;
let updateAvailable = null ; // { version, setupUrl, portableUrl, setupHash, portableHash, date }
let updateDismissedThisSession = false ;
// Simple string version compare — "0.0.4" > "0.0.3" and "0.10.0" > "0.9.9".
function versionIsNewer ( candidate , current ) {
const a = String ( candidate || "" ) . split ( "." ) . map ( ( n ) => parseInt ( n , 10 ) || 0 ) ;
const b = String ( current || "" ) . split ( "." ) . map ( ( n ) => parseInt ( n , 10 ) || 0 ) ;
const len = Math . max ( a . length , b . length ) ;
for ( let i = 0 ; i < len ; i ++ ) {
const x = a [ i ] || 0 , y = b [ i ] || 0 ;
if ( x > y ) return true ;
if ( x < y ) return false ;
}
return false ; // equal → not newer
}
async function checkForUpdate ( ) {
try {
const controller = new AbortController ( ) ;
const to = setTimeout ( ( ) => controller . abort ( ) , 5000 ) ;
const r = await fetch ( UPDATE _MANIFEST _URL , { signal : controller . signal , cache : "no-store" } ) ;
clearTimeout ( to ) ;
if ( ! r . ok ) return ;
const manifest = await r . json ( ) ;
const rel = ( manifest . releases || [ ] ) . find ( ( x ) => x . id === "theseus-navigator" ) ;
if ( ! rel || ! rel . version ) return ;
if ( ! versionIsNewer ( rel . version , app . getVersion ( ) ) ) {
// Same version or older — nothing to offer. Clear any stale state so the
// chip disappears after the user has updated + relaunched.
if ( updateAvailable ) { updateAvailable = null ; emitUpdateAvailable ( ) ; }
return ;
}
const files = rel . files || { } ;
const setupFile = Object . keys ( files ) . find ( ( k ) => / Setup / i . test ( k ) ) ;
const portableFile = Object . keys ( files ) . find ( ( k ) => / portable / i . test ( k ) ) ;
updateAvailable = {
version : rel . version ,
date : rel . date || "" ,
setupUrl : setupFile ? UPDATE _DOWNLOAD _BASE + setupFile : null ,
portableUrl : portableFile ? UPDATE _DOWNLOAD _BASE + portableFile : null ,
setupHash : setupFile ? files [ setupFile ] : null ,
portableHash : portableFile ? files [ portableFile ] : null ,
} ;
emitUpdateAvailable ( ) ;
} catch { /* offline / manifest unreachable — silent */ }
}
function emitUpdateAvailable ( ) {
const payload = ( updateDismissedThisSession || ! updateAvailable ) ? null : updateAvailable ;
try { chrome ? . webContents . send ( "update-available" , payload ) ; } catch { }
}
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
// ---- home page editable cards (userData/home-cards.json) ------------------
// Rendered by home.html as the "quick links" grid on the new-tab page. User
// can add/edit/remove via the page's edit mode. First-run seed = the classic
// Silent Mode showcase (hello.bch, theseus.bch, silentmode.bch, etc.).
const DEFAULT _HOME _CARDS = [
{ title : "hello.bch" , url : "https://hello.bch/" , sub : "On the blockchain itself." , badge : "on-chain" } ,
{ title : "theseus.bch" , url : "https://theseus.bch/" , sub : "This browser's own name." , badge : "on-chain" } ,
{ title : "silentmode.bch" , url : "https://silentmode.bch/" , sub : "The division behind this stack." , badge : "on-chain" } ,
{ title : "argo.bch" , url : "https://argo.bch/" , sub : "Host network, crewed by Argonauts." , badge : "on-chain" } ,
{ title : "coinspectrum.deviant.bch" , url : "https://coinspectrum.deviant.bch/" , sub : "A full site from Sia." , badge : "Sia" } ,
{ title : "coinspectrum.bch" , url : "https://coinspectrum.bch/" , sub : "Same site, direct server." , badge : "server" } ,
{ title : "faucet.deviant.bch" , url : "https://faucet.deviant.bch/" , sub : "Testnet faucet hub, on Sia." , badge : "Sia" } ,
{ title : "siatest.bch" , url : "https://siatest.bch/" , sub : "A page with no server." , badge : "Sia" } ,
] ;
const homeCardsFile = ( ) => path . join ( app . getPath ( "userData" ) , "home-cards.json" ) ;
function loadHomeCards ( ) {
try {
if ( fs . existsSync ( homeCardsFile ( ) ) ) {
const v = JSON . parse ( fs . readFileSync ( homeCardsFile ( ) , "utf8" ) ) ;
if ( Array . isArray ( v ) ) return v ;
}
} catch ( e ) { console . error ( "home cards load failed:" , e . message ) ; }
return DEFAULT _HOME _CARDS . slice ( ) ;
}
function saveHomeCards ( cards ) {
try { fs . writeFileSync ( homeCardsFile ( ) , JSON . stringify ( cards , null , 2 ) ) ; }
catch ( e ) { console . error ( "home cards save failed:" , e . message ) ; }
}
// Sender validation — only accept IPC from our own home.html file:// URL.
// Rejects third-party pages that see the API shape via the preload.
function isHomePageSender ( sender ) {
try {
const u = sender . getURL ( ) || "" ;
return u . startsWith ( "file://" ) && /home\.html(?:$|\?|#)/i . test ( u ) ;
} catch { return false ; }
}
Theseus 0.1.3: branded error page for load failures (BUILT, NOT DEPLOYED)
Setup f2afc14efc63008cbb9dad44176e94146386db4c0afda4459f1d4eb929172b6d
Portable 5d08b1415526934db8de780949a610896064fe9567aa0e5e1702ebabd7eb7df2
Chromium's default 'This site can't be reached' replaced with a Theseus-
themed error page. did-fail-load on every tab's webContents (main frame
only, non-ignorable code) routes the tab to error.html with the
attempt URL, host, error code, and description as query params. The
page keeps t.url pointing at the failed URL so the address bar shows
what the user typed and they can edit + retry - refreshTabUrl's
existing file:// skip means the error page's own path never leaks
back into the bar.
Five kinds, chosen by pickErrorKind(code, host):
name-not-registered BCNR-eligible host + ERR_NAME_NOT_RESOLVED.
Says "no BCDN record on chain, no clearnet host
either." Offers Register on Sirius + Search +
Retry + Home.
name-unreachable ERR_NAME_NOT_RESOLVED on a non-BCNR host. DNS
failed - offers Retry + Search + Register +
Home.
unreachable CONN_REFUSED/RESET/TIMED_OUT/CLOSED/NETWORK_CHANGED.
Offers Retry + Tor guide + Home.
tls ERR_CERT_* range (-200..-299). Offers Retry +
Home.
generic Everything else.
home-preload.js gains `window.errorpage` alongside `window.home`. Both
APIs are sender-URL-gated in main - a random page seeing the shape
can't invoke them (isErrorPageSender / isHomePageSender). The external-
open handler additionally allowlists Silent Mode domains only.
package.json build.files gets error.html + error-preload.js so
electron-builder actually bundles them (GOTCHAS rule: an unlisted
runtime-loaded file silently opens blank).
Ship pages (releases-manifest.json, tools/index.html, releases/index.html,
site-theseus-x/index.html) updated to 0.1.3 with the new hashes.
DEPLOY STATUS - blocked on VPS SSH: my IP was hit with a full-port ban
mid-turn (likely fail2ban from the burst of scp during the 0.1.0-0.1.2
iterations). Site pages/manifest/installers are committed locally but
NOT yet on dl.silentmode.st or the Sia mirror. Live still reads 0.1.2.
User needs to unban 195.184.247.106 on their end, or wait for the ban
to expire, before the ship pages match reality.
2026-08-31 13:38:05 +02:00
// Same origin-gating pattern for the branded error page.
function isErrorPageSender ( sender ) {
try {
const u = sender . getURL ( ) || "" ;
return u . startsWith ( "file://" ) && /error\.html(?:$|\?|#)/i . test ( u ) ;
} catch { return false ; }
}
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
// ---- address-bar history (userData/history.json) --------------------------
// Suggestions dropdown source. Deduped LRU capped at HISTORY_CAP entries.
// Cleared on quit when settings.clearHistoryOnQuit is on (default).
const HISTORY _CAP = 500 ;
let history = [ ] ; // [{ url, title, ts }]
let historySaveTimer = null ;
const historyFile = ( ) => path . join ( app . getPath ( "userData" ) , "history.json" ) ;
function loadHistory ( ) { try { if ( fs . existsSync ( historyFile ( ) ) ) history = JSON . parse ( fs . readFileSync ( historyFile ( ) , "utf8" ) ) ; } catch ( e ) { console . error ( "history load failed:" , e . message ) ; history = [ ] ; } }
function saveHistoryDebounced ( ) {
clearTimeout ( historySaveTimer ) ;
historySaveTimer = setTimeout ( ( ) => {
try { fs . writeFileSync ( historyFile ( ) , JSON . stringify ( history ) ) ; } catch ( e ) { console . error ( "history save failed:" , e . message ) ; }
} , 800 ) ;
}
function historyAdd ( url , title ) {
if ( ! url ) return ;
const clean = String ( url ) . trim ( ) ;
// Skip internal / non-http(s) URLs — never useful in address suggestions.
if ( ! /^https?:\/\//i . test ( clean ) && ! /^bns:\/\//i . test ( clean ) ) return ;
// LRU: remove any existing entry for this URL, unshift a fresh one to the top.
const i = history . findIndex ( ( h ) => h . url === clean ) ;
if ( i >= 0 ) history . splice ( i , 1 ) ;
history . unshift ( { url : clean , title : String ( title || "" ) . slice ( 0 , 200 ) , ts : Date . now ( ) } ) ;
if ( history . length > HISTORY _CAP ) history . length = HISTORY _CAP ;
saveHistoryDebounced ( ) ;
}
// Rank matches: prefix-of-host wins, then prefix-of-URL, then contains,
// then recency. Cap results — the dropdown wants at most ~8 entries.
function historySearch ( query , cap = 8 ) {
const q = String ( query || "" ) . trim ( ) . toLowerCase ( ) ;
if ( ! q ) return history . slice ( 0 , cap ) ;
const scored = [ ] ;
for ( const h of history ) {
const u = h . url . toLowerCase ( ) ;
const host = u . replace ( /^https?:\/\// , "" ) . split ( /[/?#]/ ) [ 0 ] ;
let score ;
if ( host . startsWith ( q ) ) score = 100 ;
else if ( u . startsWith ( q ) ) score = 80 ;
else if ( host . includes ( q ) ) score = 60 ;
else if ( u . includes ( q ) ) score = 40 ;
else if ( ( h . title || "" ) . toLowerCase ( ) . includes ( q ) ) score = 20 ;
else continue ;
scored . push ( { h , score } ) ;
}
scored . sort ( ( a , b ) => ( b . score - a . score ) || ( b . h . ts - a . h . ts ) ) ;
return scored . slice ( 0 , cap ) . map ( ( s ) => s . h ) ;
}
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 ) ; }
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
// Storage retention — Chromium/Electron sessions accumulate cookies, HTTP
// cache, localStorage, IndexedDB, service workers, cache API by default.
// This wipes whichever the caller asked for. The `storages` list mirrors
// Chromium's clearStorageData taxonomy — we group them into a small user-
// facing bucket ("cookies" / "cache" / "storage") so settings stay simple.
async function clearBrowsingData ( { cookies = false , cache = false , storage = false } = { } ) {
const ses = session . defaultSession ;
if ( cache ) { try { await ses . clearCache ( ) ; } catch ( e ) { console . warn ( "clearCache:" , e . message ) ; } }
const storages = [ ] ;
if ( cookies ) storages . push ( "cookies" ) ;
if ( storage ) storages . push ( "localstorage" , "indexdb" , "serviceworkers" , "cachestorage" , "shadercache" ) ;
if ( storages . length ) {
try { await ses . clearStorageData ( { storages } ) ; }
catch ( e ) { console . warn ( "clearStorageData:" , e . message ) ; }
}
// navigation history lives in each webContents; drop it too when history-clear was asked.
// (called separately by the before-quit hook, since history-clear also deletes session.json)
}
async function clearHistoryNow ( ) {
for ( const t of tabs ) {
try { t . view . webContents . navigationHistory . clear ( ) ; } catch { }
}
try { fs . unlinkSync ( sessionFile ( ) ) ; } catch { }
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
// Address-bar suggestions history — wipe both in-memory + on-disk.
history = [ ] ;
clearTimeout ( historySaveTimer ) ; historySaveTimer = null ;
try { fs . unlinkSync ( historyFile ( ) ) ; } catch { }
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
}
2026-07-29 13:54:34 +02:00
// 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 ;
} ) ;
}
2026-08-05 18:38:38 +02:00
// Cookie shim for cross-site embeds. Sites like the faucet hub's captcha-gated testnet
// faucets set session cookies with no SameSite attribute; Chromium defaults those to
// Lax and withholds them inside cross-site iframes, so cookie-bound captcha endpoints
// fail (tbch.googol.cash /captcha 500s without its session cookie). Rewriting their
// Set-Cookie to SameSite=None; Secure makes the cookie frame-eligible. Allowlist only —
// SameSite is CSRF protection, never relax it globally. NOTE: Electron keeps a single
// onHeadersReceived listener per session; if another is ever added, merge them.
const EMBED _COOKIE _SITES = [ "https://tbch.googol.cash/*" , "https://signetfaucet.com/*" ] ;
function applyEmbedCookieShim ( ) {
session . defaultSession . webRequest . onHeadersReceived ( { urls : EMBED _COOKIE _SITES } , ( details , callback ) => {
const headers = details . responseHeaders || { } ;
for ( const key of Object . keys ( headers ) ) {
if ( key . toLowerCase ( ) !== "set-cookie" ) continue ;
headers [ key ] = headers [ key ] . map ( ( c ) => ( /;\s*samesite=/i . test ( c ) ? c : c + "; SameSite=None; Secure" ) ) ;
}
callback ( { responseHeaders : headers } ) ;
} ) ;
}
2026-07-29 13:54:34 +02:00
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 ( ) ) } ;
}
gateway+Theseus: pin BNS ip-record fetch to on-chain tls fingerprint
Uncovered by the 2026-08-13 subdomain-inheritance fix: once
`checkers.game.x` correctly picked the parent's `ip` record instead of
`s3`, the ip branch itself failed. Two reasons:
1. `fetch("http://<ip>/", { headers: { host: name } })` follows the
site's :80→:443 redirect into `https://<name>.<tld>/`, which isn't
in ICANN DNS → "fetch failed".
2. The site's cert is signed by a per-machine BNS root, not a public
CA; standard TLS validation rejects it.
Both are fixed by connecting to the IP with SNI = name, pinning the
presented cert's SHA-256 against the on-chain `tls` record, and only
then issuing the HTTPS request over the same socket. The on-chain
fingerprint is the trust anchor BNS uses everywhere else (see
Argus/src/lib/ca.js).
Gateway (public-gateway.mjs): new pinnedHttpsGet + httpGet + ipRequest
helpers; case "ip" delegates. No silent HTTP fallback on pin failure
(a mismatch means "not the site the chain says it is").
Theseus (main.js): parallel port of the same helpers, Tor-aware
(routes through SocksProxyAgent when Tor is on). serveBns's inner
serveIp() delegates to ipRequest.
Verified live: `curl -sI https://navigate.st/bns/checkers.game.x/`
returns 200 OK with the checkers game (1,179,215 bytes, apex
`game.x` unchanged, served from Sia).
2026-08-16 20:28:58 +02:00
// BNS `ip`-record fetch. The default `fetch` fails here for two reasons:
// 1. It follows the site's HTTP→HTTPS 301 into `https://<name>.<tld>/`, which
// isn't in ICANN DNS → "fetch failed".
// 2. It validates TLS against the public CA store, but BNS certs are signed
// by per-machine BNS root CAs — the trust anchor is the on-chain `tls`
// record's SHA-256 fingerprint, which we pin against here. See
// Argus/src/lib/ca.js for the underlying trust model, and the parallel
// implementation in Argus/src/gateway/public-gateway.mjs — keep both in
// step. This code path also runs through Tor when Tor is on.
function certFp ( cert ) {
const fp = cert && cert . fingerprint256 ;
return fp ? fp . toLowerCase ( ) . replace ( /:/g , "" ) : "" ;
}
async function pinnedHttpsGet ( ip , port , servername , reqPath , expectedFp ) {
const useTor = torState === "on" ;
if ( useTor ) await loadSocks ( ) ;
return new Promise ( ( resolve , reject ) => {
const opts = {
host : ip , port , servername , method : "GET" , path : reqPath ,
headers : { host : servername , "user-agent" : "theseus/1" } ,
} ;
opts [ "rejectUnauthorized" ] = false ; // fingerprint pin below is the gate.
if ( useTor ) opts . agent = new SocksProxyAgent ( ` socks5h://127.0.0.1: ${ TOR _PORT } ` ) ;
const req = https . request ( opts , ( res ) => {
const gotFp = certFp ( res . socket . getPeerCertificate ( false ) ) ;
if ( gotFp !== expectedFp ) {
res . socket . destroy ( ) ;
return reject ( new Error ( ` tls fingerprint mismatch for ${ servername } : got ${ gotFp } , expected ${ expectedFp } ` ) ) ;
}
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 . setTimeout ( 15000 , ( ) => { req . destroy ( new Error ( "tls request timeout" ) ) ; } ) ;
req . on ( "error" , reject ) ;
req . end ( ) ;
} ) ;
}
async function httpGetByIp ( ip , reqPath , hostHeader ) {
const useTor = torState === "on" ;
if ( useTor ) await loadSocks ( ) ;
return new Promise ( ( resolve , reject ) => {
const opts = {
host : ip , port : 80 , method : "GET" , path : reqPath ,
headers : { host : hostHeader , "user-agent" : "theseus/1" } ,
} ;
if ( useTor ) opts . agent = new SocksProxyAgent ( ` socks5h://127.0.0.1: ${ TOR _PORT } ` ) ;
const req = http . request ( opts , ( 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 ( ) ;
} ) ;
}
// Compose: pinned HTTPS if the on-chain `tls` fingerprint is available, else
// plain HTTP by IP. No silent HTTP fallback on pin failure — a mismatch means
// "not the site the chain says it is" and returning HTTP anyway would defeat
// the pin.
async function ipRequest ( ip , reqPath , hostHeader , tlsFingerprint ) {
if ( tlsFingerprint ) return await pinnedHttpsGet ( ip , 443 , hostHeader , reqPath , String ( tlsFingerprint ) . toLowerCase ( ) ) ;
return await httpGetByIp ( ip , reqPath , hostHeader ) ;
}
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 ;
BNS: Sia+Nostr snapshot mirror, Theseus warm-start, VPS playbook, electrum.bch
Four independent pieces of one story — make cold-start .bch resolution fast,
survive individual electrum outages, and turn adding a Silent Mode BNS server
into a checklist run.
* Argus/src/lib/snapshot-name.js — pure, interpretation-free snapshot format
for the name beacon (raw history + verbose tx cache + deterministic root).
* Argus/src/publish-name-mirror.mjs — mirror the snapshot to Sia S3
(floating + content-addressed) and Nostr NIP-33 (kind 30078,
d-tag bns-name-list). Same pattern as publish-tld-mirror.mjs.
* Argus/src/snapshot-name-to-file.mjs — dump the snapshot to a file for
bundling. Writes to TheseusNavigator/snapshots/bns-name-snapshot.json.
* Argus/src/lib/resolver-web.js — new export buildIndexFromSnapshot() runs
the same REG/UPD reduction as buildIndex() but from a pre-fetched snapshot,
no electrum required. Warm-start path in Theseus depends on this.
* TheseusNavigator/main.js — warmFromSnapshot() and refreshSnapshotFromSia()
wired into app.whenReady(). First .bch navigation returns from a warm index
instead of waiting on a full electrum walk.
* TheseusNavigator/package.json — bundles the starter snapshot as an
extraResource so packaged builds ship with a floor.
* TheseusNavigator/snapshots/bns-name-snapshot.json — first starter snapshot
(70 beacon txs, root 8cc859aa…).
* VPS/BNS-SERVER-PLAYBOOK.md — turn-key checklist for spinning up a second
or third Silent Mode BNS server, distilled from what actually worked on
the silentmode box (BCHN 29 [chip]-not-[chipnet], multi-beacon indexer,
wss:// nginx, on-chain publish via electrum.bch, common gotchas table).
* Argus/src/register-electrum-list.mjs — publish the current chipnet
server pool as `el` on electrum.bch. Fits inside the 200-byte OP_RETURN
cap by sizing the record (drops fallback IP pins in favour of URL-only
tokens until they fit). NAME was chosen deliberately: two-part `electrum.bch`
normalises to the bare distinct name `electrum`, whereas the earlier
candidate `electrum.silentmode.bch` collapsed to silentmode.bch and would
have inherited/overwritten that name's records.
* Argus/src/revert-silentmode-bch.mjs — one-off recovery from the earlier
register-electrum-list revision that hit exactly that subdomain-collapse
bug (broadcast an UPD to silentmode.bch and clobbered its s3 record).
Records recovered from the local snapshot cache; safe to leave in the
tree as documentation of what happened.
2026-08-30 09:59:01 +02:00
// ---- warm-start from a pre-fetched beacon snapshot ------------------------
//
// The first ensureIndex() call walks the whole beacon over electrum — that's
// the "empty tab spinner" a user sees on cold start. The snapshot pipeline
// (Argus/src/publish-name-mirror.mjs) makes that walk skippable: an operator
// publishes the raw history+txs to Sia; every Theseus install carries a
// starter snapshot bundled at build time, then GETs a fresher one on boot.
// The warm sharedIndex is served immediately; the live buildIndex runs in the
// background to catch any events past the snapshot's asOfHeight.
//
// Sources tried in order:
// 1. app.getPath("userData")/bns-name-snapshot.json — the fresher copy
// written on our last successful Sia refresh (persisted across launches)
// 2. RES_DIR/bns-name-snapshot.json — the copy bundled with the build (stale
// by definition, but strictly better than "no index at all")
// Both are optional; if neither exists, ensureIndex() does what it always did
// and the user sees the same cold-start experience as before this change.
const SNAPSHOT _BUNDLED = path . join ( RES _DIR , "bns-name-snapshot.json" ) ;
const SIA _SNAPSHOT _URL = "https://s3.silentmode.st:8600/bns/name-list.json" ;
function snapshotUserPath ( ) { return path . join ( app . getPath ( "userData" ) , "bns-name-snapshot.json" ) ; }
function readSnapshotFrom ( p ) {
try {
if ( ! fs . existsSync ( p ) ) return null ;
const parsed = JSON . parse ( fs . readFileSync ( p , "utf8" ) ) ;
// A minimal shape check — buildIndexFromSnapshot will throw with a
// clear message on anything else, but we want to log which source
// was chosen for diagnostics.
if ( ! parsed || ! Array . isArray ( parsed . history ) ) return null ;
return parsed ;
} catch { return null ; }
}
Theseus 0.0.7: multi-source BNS with continuous delta refresh
The first .bch page now opens with zero user-visible latency and stays fresh
for as long as the browser runs. Four sources conspire in parallel — none of
them can block a navigation:
1. Warm start from disk (sync)
warmFromSnapshot loads the user-cache snapshot first, then falls back to
the copy bundled with the build. sharedIndex is set BEFORE the first
navigation can even fire.
2. Continuous background delta refresh (every 30 s)
startBnsPolling opens one electrum connection, fetches the beacon
history (a single call), and only pulls verbose tx bodies for txids we
don't already have — mergeFreshHistory-style. Merges into memory,
rebuilds the index locally, persists the enlarged snapshot to disk.
Turns a ~60 s full walk into ~1 s per new event.
3. Sia snapshot pull on boot (one-shot, wins the NEXT boot)
refreshSnapshotFromSia downloads the operator's published snapshot from
s3.silentmode.st for the next launch. After a long idle period the
browser resumes from that fresher snapshot instead of walking days of
events.
4. ensureIndex full-walk fallback
Still runs on boot for the case where there's no bundled snapshot AND
the poll hasn't landed yet — very first launch, offline install, etc.
resolveHost now prefers sharedIndex (which the poll keeps live) and, on a
miss with a stale index, triggers pollAndMerge (delta fetch) instead of
ensureIndex (full walk). The old full-walk fallback is only taken if the
delta primitives aren't available (running against an older resolver-web).
Argus/src/lib/resolver-web.js exports connectElectrum so the Theseus poll
can drive its own ad-hoc queries against the pool without duplicating the
Electrum wrapper.
Bundled starter snapshot refreshed: 73 beacon txs, root c37b859682…54e414ba.
Artifacts:
dist-public/TheseusNavigator-Setup-0.0.7.exe (95.4 MB)
sha256 1b0dabcd2b13067aa1fd89c3271708b35ba22dfc7a620e248b35e0487b7a104f
dist-public/TheseusNavigator-0.0.7-portable.exe (92.7 MB)
sha256 706dc01b21a88d3c2fbc3eeced0cab6ace873630bf3edc2d40915ca5be5c8cae
2026-08-30 15:57:01 +02:00
// In-memory copy of the raw snapshot state (`{beacon, history, txs, ...}`)
// that drives the sharedIndex. Kept alongside sharedIndex so the delta poll
// can merge new beacon events into it without re-reading from disk on every
// refresh. Written to disk after each successful merge — the user cache is
// always the most up-to-date snapshot this process knows about, so a restart
// resumes from where we left off instead of from the stale bundled copy.
let currentSnapshotState = null ;
BNS: Sia+Nostr snapshot mirror, Theseus warm-start, VPS playbook, electrum.bch
Four independent pieces of one story — make cold-start .bch resolution fast,
survive individual electrum outages, and turn adding a Silent Mode BNS server
into a checklist run.
* Argus/src/lib/snapshot-name.js — pure, interpretation-free snapshot format
for the name beacon (raw history + verbose tx cache + deterministic root).
* Argus/src/publish-name-mirror.mjs — mirror the snapshot to Sia S3
(floating + content-addressed) and Nostr NIP-33 (kind 30078,
d-tag bns-name-list). Same pattern as publish-tld-mirror.mjs.
* Argus/src/snapshot-name-to-file.mjs — dump the snapshot to a file for
bundling. Writes to TheseusNavigator/snapshots/bns-name-snapshot.json.
* Argus/src/lib/resolver-web.js — new export buildIndexFromSnapshot() runs
the same REG/UPD reduction as buildIndex() but from a pre-fetched snapshot,
no electrum required. Warm-start path in Theseus depends on this.
* TheseusNavigator/main.js — warmFromSnapshot() and refreshSnapshotFromSia()
wired into app.whenReady(). First .bch navigation returns from a warm index
instead of waiting on a full electrum walk.
* TheseusNavigator/package.json — bundles the starter snapshot as an
extraResource so packaged builds ship with a floor.
* TheseusNavigator/snapshots/bns-name-snapshot.json — first starter snapshot
(70 beacon txs, root 8cc859aa…).
* VPS/BNS-SERVER-PLAYBOOK.md — turn-key checklist for spinning up a second
or third Silent Mode BNS server, distilled from what actually worked on
the silentmode box (BCHN 29 [chip]-not-[chipnet], multi-beacon indexer,
wss:// nginx, on-chain publish via electrum.bch, common gotchas table).
* Argus/src/register-electrum-list.mjs — publish the current chipnet
server pool as `el` on electrum.bch. Fits inside the 200-byte OP_RETURN
cap by sizing the record (drops fallback IP pins in favour of URL-only
tokens until they fit). NAME was chosen deliberately: two-part `electrum.bch`
normalises to the bare distinct name `electrum`, whereas the earlier
candidate `electrum.silentmode.bch` collapsed to silentmode.bch and would
have inherited/overwritten that name's records.
* Argus/src/revert-silentmode-bch.mjs — one-off recovery from the earlier
register-electrum-list revision that hit exactly that subdomain-collapse
bug (broadcast an UPD to silentmode.bch and clobbered its s3 record).
Records recovered from the local snapshot cache; safe to leave in the
tree as documentation of what happened.
2026-08-30 09:59:01 +02:00
async function warmFromSnapshot ( ) {
if ( sharedIndex ) return sharedIndex ; // already warm — nothing to do
const { buildIndexFromSnapshot } = await getResolver ( ) ;
if ( ! buildIndexFromSnapshot ) return null ; // running against an older resolver-web.js
const snap = readSnapshotFrom ( snapshotUserPath ( ) ) || readSnapshotFrom ( SNAPSHOT _BUNDLED ) ;
if ( ! snap ) return null ;
try {
const idx = buildIndexFromSnapshot ( { snapshot : snap } ) ;
sharedIndex = idx ;
Theseus 0.0.7: multi-source BNS with continuous delta refresh
The first .bch page now opens with zero user-visible latency and stays fresh
for as long as the browser runs. Four sources conspire in parallel — none of
them can block a navigation:
1. Warm start from disk (sync)
warmFromSnapshot loads the user-cache snapshot first, then falls back to
the copy bundled with the build. sharedIndex is set BEFORE the first
navigation can even fire.
2. Continuous background delta refresh (every 30 s)
startBnsPolling opens one electrum connection, fetches the beacon
history (a single call), and only pulls verbose tx bodies for txids we
don't already have — mergeFreshHistory-style. Merges into memory,
rebuilds the index locally, persists the enlarged snapshot to disk.
Turns a ~60 s full walk into ~1 s per new event.
3. Sia snapshot pull on boot (one-shot, wins the NEXT boot)
refreshSnapshotFromSia downloads the operator's published snapshot from
s3.silentmode.st for the next launch. After a long idle period the
browser resumes from that fresher snapshot instead of walking days of
events.
4. ensureIndex full-walk fallback
Still runs on boot for the case where there's no bundled snapshot AND
the poll hasn't landed yet — very first launch, offline install, etc.
resolveHost now prefers sharedIndex (which the poll keeps live) and, on a
miss with a stale index, triggers pollAndMerge (delta fetch) instead of
ensureIndex (full walk). The old full-walk fallback is only taken if the
delta primitives aren't available (running against an older resolver-web).
Argus/src/lib/resolver-web.js exports connectElectrum so the Theseus poll
can drive its own ad-hoc queries against the pool without duplicating the
Electrum wrapper.
Bundled starter snapshot refreshed: 73 beacon txs, root c37b859682…54e414ba.
Artifacts:
dist-public/TheseusNavigator-Setup-0.0.7.exe (95.4 MB)
sha256 1b0dabcd2b13067aa1fd89c3271708b35ba22dfc7a620e248b35e0487b7a104f
dist-public/TheseusNavigator-0.0.7-portable.exe (92.7 MB)
sha256 706dc01b21a88d3c2fbc3eeced0cab6ace873630bf3edc2d40915ca5be5c8cae
2026-08-30 15:57:01 +02:00
currentSnapshotState = snap ;
BNS: Sia+Nostr snapshot mirror, Theseus warm-start, VPS playbook, electrum.bch
Four independent pieces of one story — make cold-start .bch resolution fast,
survive individual electrum outages, and turn adding a Silent Mode BNS server
into a checklist run.
* Argus/src/lib/snapshot-name.js — pure, interpretation-free snapshot format
for the name beacon (raw history + verbose tx cache + deterministic root).
* Argus/src/publish-name-mirror.mjs — mirror the snapshot to Sia S3
(floating + content-addressed) and Nostr NIP-33 (kind 30078,
d-tag bns-name-list). Same pattern as publish-tld-mirror.mjs.
* Argus/src/snapshot-name-to-file.mjs — dump the snapshot to a file for
bundling. Writes to TheseusNavigator/snapshots/bns-name-snapshot.json.
* Argus/src/lib/resolver-web.js — new export buildIndexFromSnapshot() runs
the same REG/UPD reduction as buildIndex() but from a pre-fetched snapshot,
no electrum required. Warm-start path in Theseus depends on this.
* TheseusNavigator/main.js — warmFromSnapshot() and refreshSnapshotFromSia()
wired into app.whenReady(). First .bch navigation returns from a warm index
instead of waiting on a full electrum walk.
* TheseusNavigator/package.json — bundles the starter snapshot as an
extraResource so packaged builds ship with a floor.
* TheseusNavigator/snapshots/bns-name-snapshot.json — first starter snapshot
(70 beacon txs, root 8cc859aa…).
* VPS/BNS-SERVER-PLAYBOOK.md — turn-key checklist for spinning up a second
or third Silent Mode BNS server, distilled from what actually worked on
the silentmode box (BCHN 29 [chip]-not-[chipnet], multi-beacon indexer,
wss:// nginx, on-chain publish via electrum.bch, common gotchas table).
* Argus/src/register-electrum-list.mjs — publish the current chipnet
server pool as `el` on electrum.bch. Fits inside the 200-byte OP_RETURN
cap by sizing the record (drops fallback IP pins in favour of URL-only
tokens until they fit). NAME was chosen deliberately: two-part `electrum.bch`
normalises to the bare distinct name `electrum`, whereas the earlier
candidate `electrum.silentmode.bch` collapsed to silentmode.bch and would
have inherited/overwritten that name's records.
* Argus/src/revert-silentmode-bch.mjs — one-off recovery from the earlier
register-electrum-list revision that hit exactly that subdomain-collapse
bug (broadcast an UPD to silentmode.bch and clobbered its s3 record).
Records recovered from the local snapshot cache; safe to leave in the
tree as documentation of what happened.
2026-08-30 09:59:01 +02:00
// Deliberately set indexBuiltAt to 0 so the first real navigation still
// triggers a live refresh — the snapshot is a floor, not a ceiling.
indexBuiltAt = 0 ;
refreshBcnrTlds ( idx ) ;
console . log ( ` [bns] warm-started from snapshot: ${ idx . size } names @ height= ${ snap . asOfHeight ? ? "?" } root= ${ snap . root ? ? "?" } ` ) ;
return idx ;
} catch ( e ) {
console . warn ( "[bns] snapshot warm-start failed:" , e . message ) ;
return null ;
}
}
Theseus 0.0.7: multi-source BNS with continuous delta refresh
The first .bch page now opens with zero user-visible latency and stays fresh
for as long as the browser runs. Four sources conspire in parallel — none of
them can block a navigation:
1. Warm start from disk (sync)
warmFromSnapshot loads the user-cache snapshot first, then falls back to
the copy bundled with the build. sharedIndex is set BEFORE the first
navigation can even fire.
2. Continuous background delta refresh (every 30 s)
startBnsPolling opens one electrum connection, fetches the beacon
history (a single call), and only pulls verbose tx bodies for txids we
don't already have — mergeFreshHistory-style. Merges into memory,
rebuilds the index locally, persists the enlarged snapshot to disk.
Turns a ~60 s full walk into ~1 s per new event.
3. Sia snapshot pull on boot (one-shot, wins the NEXT boot)
refreshSnapshotFromSia downloads the operator's published snapshot from
s3.silentmode.st for the next launch. After a long idle period the
browser resumes from that fresher snapshot instead of walking days of
events.
4. ensureIndex full-walk fallback
Still runs on boot for the case where there's no bundled snapshot AND
the poll hasn't landed yet — very first launch, offline install, etc.
resolveHost now prefers sharedIndex (which the poll keeps live) and, on a
miss with a stale index, triggers pollAndMerge (delta fetch) instead of
ensureIndex (full walk). The old full-walk fallback is only taken if the
delta primitives aren't available (running against an older resolver-web).
Argus/src/lib/resolver-web.js exports connectElectrum so the Theseus poll
can drive its own ad-hoc queries against the pool without duplicating the
Electrum wrapper.
Bundled starter snapshot refreshed: 73 beacon txs, root c37b859682…54e414ba.
Artifacts:
dist-public/TheseusNavigator-Setup-0.0.7.exe (95.4 MB)
sha256 1b0dabcd2b13067aa1fd89c3271708b35ba22dfc7a620e248b35e0487b7a104f
dist-public/TheseusNavigator-0.0.7-portable.exe (92.7 MB)
sha256 706dc01b21a88d3c2fbc3eeced0cab6ace873630bf3edc2d40915ca5be5c8cae
2026-08-30 15:57:01 +02:00
// ---- continuous background delta refresh --------------------------------
//
// Every POLL_INTERVAL_MS the browser opens ONE electrum connection, fetches
// the beacon's current history (a single fast call), diffs it against the
// snapshot we already hold in memory, and only fetches the verbose tx bodies
// for the txids we don't have yet. Then we rebuild the index locally and
// persist the enlarged snapshot to disk.
//
// This turns "index refresh" from ~60 s of round-trips (fetch every verbose
// tx for the whole beacon) into ~1 s of round-trips per new event. And
// because it runs while the browser is idle, by the time the user actually
// types a name into the URL bar there is nothing to wait for.
//
// Sources conspiring for freshness:
// * warmFromSnapshot on boot — sharedIndex is warm before nav
// * this poll loop, every 30 s — keeps sharedIndex live and current
// * refreshSnapshotFromSia on boot — pulls the operator's published
// snapshot from Sia for the NEXT
// boot; if this browser was closed
// for a week, next launch skips
// days of catch-up
// * ensureIndex still exists — full-walk fallback for the case
// where the poll cannot connect
// (offline first launch, etc.)
const POLL _INTERVAL _MS = 30_000 ;
let pollInFlight = null ;
let pollTimer = null ;
let pollAttempts = 0 , pollLastError = null ;
async function pollAndMerge ( ) {
if ( pollInFlight ) return pollInFlight ;
pollInFlight = ( async ( ) => {
pollAttempts ++ ;
try {
const R = await getResolver ( ) ;
if ( ! R . connectElectrum || ! R . BEACON _SCRIPTHASH || ! R . buildIndexFromSnapshot ) {
// Older resolver-web without the delta primitives — nothing to do.
return null ;
}
if ( ! electrumPool ) await initElectrumPool ( ) ;
// Base state: memory > user cache > bundled > empty. The "empty" branch
// is what turns the very first cold start (no bundled snapshot present
// because we shipped a build that predates snapshotting) into a full
// rebuild — mergeFreshHistory will fetch every tx.
let snap = currentSnapshotState
|| readSnapshotFrom ( snapshotUserPath ( ) )
|| readSnapshotFrom ( SNAPSHOT _BUNDLED )
|| { beacon : R . BEACON _SCRIPTHASH , history : [ ] , txs : { } } ;
const el = await R . connectElectrum ( {
electrum : electrumPool , WebSocket : currentWS ( ) , directIP : true ,
} ) ;
try {
const freshHistory = await el . call ( "blockchain.scripthash.get_history" , [ R . BEACON _SCRIPTHASH ] ) ;
const known = new Set ( snap . history . map ( ( h ) => h . tx _hash ) ) ;
// Merge fresh into snapshot history (dedup by tx_hash, keep fresh height —
// an event that was mempool at snapshot time now has a real height).
const merged = new Map ( snap . history . map ( ( h ) => [ h . tx _hash , h ] ) ) ;
const txs = { ... ( snap . txs || { } ) } ;
let added = 0 ;
for ( const h of freshHistory ) {
if ( ! known . has ( h . tx _hash ) ) {
try {
txs [ h . tx _hash ] = await el . call ( "blockchain.transaction.get" , [ h . tx _hash , true ] ) ;
added ++ ;
} catch { /* unreadable — the reduction rules ignore missing txs */ }
}
merged . set ( h . tx _hash , { tx _hash : h . tx _hash , height : h . height } ) ;
}
const history = [ ... merged . values ( ) ] ;
currentSnapshotState = { ... snap , beacon : R . BEACON _SCRIPTHASH , history , txs } ;
const idx = R . buildIndexFromSnapshot ( { snapshot : currentSnapshotState } ) ;
sharedIndex = idx ;
indexBuiltAt = Date . now ( ) ;
pollLastError = null ;
refreshBcnrTlds ( idx ) ;
// Persist for the next launch. Failure here is not fatal — worst case
// we redo this merge on the next start.
try {
fs . mkdirSync ( path . dirname ( snapshotUserPath ( ) ) , { recursive : true } ) ;
fs . writeFileSync ( snapshotUserPath ( ) , JSON . stringify ( currentSnapshotState ) ) ;
} catch { /* readonly userdata / disk full — skip */ }
if ( added > 0 ) {
console . log ( ` [bns] delta-refresh: + ${ added } new tx ${ added === 1 ? "" : "s" } (total ${ history . length } , index has ${ idx . size } names) ` ) ;
}
} finally { try { el . close ( ) ; } catch { } }
} catch ( e ) {
pollLastError = e && e . message || String ( e ) ;
// Silent — the browser stays usable via sharedIndex (last-known-good) or
// the ensureIndex fallback on the next navigation.
} finally { pollInFlight = null ; }
} ) ( ) ;
return pollInFlight ;
}
function startBnsPolling ( ) {
if ( pollTimer ) return ;
// Fire immediately so the boot warm-start gets a delta pass right away, in
// parallel with the Sia refresh and the ensureIndex fallback below. Then
// every POLL_INTERVAL_MS while the browser is running.
pollAndMerge ( ) . catch ( ( ) => { } ) ;
pollTimer = setInterval ( ( ) => pollAndMerge ( ) . catch ( ( ) => { } ) , POLL _INTERVAL _MS ) ;
}
function stopBnsPolling ( ) { if ( pollTimer ) { clearInterval ( pollTimer ) ; pollTimer = null ; } }
BNS: Sia+Nostr snapshot mirror, Theseus warm-start, VPS playbook, electrum.bch
Four independent pieces of one story — make cold-start .bch resolution fast,
survive individual electrum outages, and turn adding a Silent Mode BNS server
into a checklist run.
* Argus/src/lib/snapshot-name.js — pure, interpretation-free snapshot format
for the name beacon (raw history + verbose tx cache + deterministic root).
* Argus/src/publish-name-mirror.mjs — mirror the snapshot to Sia S3
(floating + content-addressed) and Nostr NIP-33 (kind 30078,
d-tag bns-name-list). Same pattern as publish-tld-mirror.mjs.
* Argus/src/snapshot-name-to-file.mjs — dump the snapshot to a file for
bundling. Writes to TheseusNavigator/snapshots/bns-name-snapshot.json.
* Argus/src/lib/resolver-web.js — new export buildIndexFromSnapshot() runs
the same REG/UPD reduction as buildIndex() but from a pre-fetched snapshot,
no electrum required. Warm-start path in Theseus depends on this.
* TheseusNavigator/main.js — warmFromSnapshot() and refreshSnapshotFromSia()
wired into app.whenReady(). First .bch navigation returns from a warm index
instead of waiting on a full electrum walk.
* TheseusNavigator/package.json — bundles the starter snapshot as an
extraResource so packaged builds ship with a floor.
* TheseusNavigator/snapshots/bns-name-snapshot.json — first starter snapshot
(70 beacon txs, root 8cc859aa…).
* VPS/BNS-SERVER-PLAYBOOK.md — turn-key checklist for spinning up a second
or third Silent Mode BNS server, distilled from what actually worked on
the silentmode box (BCHN 29 [chip]-not-[chipnet], multi-beacon indexer,
wss:// nginx, on-chain publish via electrum.bch, common gotchas table).
* Argus/src/register-electrum-list.mjs — publish the current chipnet
server pool as `el` on electrum.bch. Fits inside the 200-byte OP_RETURN
cap by sizing the record (drops fallback IP pins in favour of URL-only
tokens until they fit). NAME was chosen deliberately: two-part `electrum.bch`
normalises to the bare distinct name `electrum`, whereas the earlier
candidate `electrum.silentmode.bch` collapsed to silentmode.bch and would
have inherited/overwritten that name's records.
* Argus/src/revert-silentmode-bch.mjs — one-off recovery from the earlier
register-electrum-list revision that hit exactly that subdomain-collapse
bug (broadcast an UPD to silentmode.bch and clobbered its s3 record).
Records recovered from the local snapshot cache; safe to leave in the
tree as documentation of what happened.
2026-08-30 09:59:01 +02:00
// Fetch the latest published snapshot from Sia and persist it as the user
// copy — the next launch (or the next warmFromSnapshot call) picks it up.
// Fire-and-forget: failures are silent; the live buildIndex path is the
// authoritative catch-up.
async function refreshSnapshotFromSia ( ) {
try {
const res = await fetch ( SIA _SNAPSHOT _URL , { redirect : "follow" } ) ;
if ( ! res . ok ) return ;
const body = await res . text ( ) ;
const parsed = JSON . parse ( body ) ;
if ( ! parsed || ! Array . isArray ( parsed . history ) ) return ;
try { fs . mkdirSync ( path . dirname ( snapshotUserPath ( ) ) , { recursive : true } ) ; } catch { }
fs . writeFileSync ( snapshotUserPath ( ) , body ) ;
console . log ( ` [bns] snapshot refreshed from Sia: ${ parsed . history . length } beacon txs root= ${ parsed . root ? ? "?" } ` ) ;
} catch { /* offline / Sia unreachable / bad JSON — the live path still works */ }
}
2026-07-30 19:29:32 +02:00
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 ; }
Theseus 0.0.7: multi-source BNS with continuous delta refresh
The first .bch page now opens with zero user-visible latency and stays fresh
for as long as the browser runs. Four sources conspire in parallel — none of
them can block a navigation:
1. Warm start from disk (sync)
warmFromSnapshot loads the user-cache snapshot first, then falls back to
the copy bundled with the build. sharedIndex is set BEFORE the first
navigation can even fire.
2. Continuous background delta refresh (every 30 s)
startBnsPolling opens one electrum connection, fetches the beacon
history (a single call), and only pulls verbose tx bodies for txids we
don't already have — mergeFreshHistory-style. Merges into memory,
rebuilds the index locally, persists the enlarged snapshot to disk.
Turns a ~60 s full walk into ~1 s per new event.
3. Sia snapshot pull on boot (one-shot, wins the NEXT boot)
refreshSnapshotFromSia downloads the operator's published snapshot from
s3.silentmode.st for the next launch. After a long idle period the
browser resumes from that fresher snapshot instead of walking days of
events.
4. ensureIndex full-walk fallback
Still runs on boot for the case where there's no bundled snapshot AND
the poll hasn't landed yet — very first launch, offline install, etc.
resolveHost now prefers sharedIndex (which the poll keeps live) and, on a
miss with a stale index, triggers pollAndMerge (delta fetch) instead of
ensureIndex (full walk). The old full-walk fallback is only taken if the
delta primitives aren't available (running against an older resolver-web).
Argus/src/lib/resolver-web.js exports connectElectrum so the Theseus poll
can drive its own ad-hoc queries against the pool without duplicating the
Electrum wrapper.
Bundled starter snapshot refreshed: 73 beacon txs, root c37b859682…54e414ba.
Artifacts:
dist-public/TheseusNavigator-Setup-0.0.7.exe (95.4 MB)
sha256 1b0dabcd2b13067aa1fd89c3271708b35ba22dfc7a620e248b35e0487b7a104f
dist-public/TheseusNavigator-0.0.7-portable.exe (92.7 MB)
sha256 706dc01b21a88d3c2fbc3eeced0cab6ace873630bf3edc2d40915ca5be5c8cae
2026-08-30 15:57:01 +02:00
// Prefer the warm sharedIndex — the poll loop keeps it live. If we don't
// have one yet (very cold start, snapshot missing AND poll hasn't landed
// yet), fall through to a full ensureIndex build.
let idx = sharedIndex || ( await ensureIndex ( ) ) ;
2026-07-30 19:29:32 +02:00
let entry = idx . get ( key ) ? ? null ;
Theseus 0.0.7: multi-source BNS with continuous delta refresh
The first .bch page now opens with zero user-visible latency and stays fresh
for as long as the browser runs. Four sources conspire in parallel — none of
them can block a navigation:
1. Warm start from disk (sync)
warmFromSnapshot loads the user-cache snapshot first, then falls back to
the copy bundled with the build. sharedIndex is set BEFORE the first
navigation can even fire.
2. Continuous background delta refresh (every 30 s)
startBnsPolling opens one electrum connection, fetches the beacon
history (a single call), and only pulls verbose tx bodies for txids we
don't already have — mergeFreshHistory-style. Merges into memory,
rebuilds the index locally, persists the enlarged snapshot to disk.
Turns a ~60 s full walk into ~1 s per new event.
3. Sia snapshot pull on boot (one-shot, wins the NEXT boot)
refreshSnapshotFromSia downloads the operator's published snapshot from
s3.silentmode.st for the next launch. After a long idle period the
browser resumes from that fresher snapshot instead of walking days of
events.
4. ensureIndex full-walk fallback
Still runs on boot for the case where there's no bundled snapshot AND
the poll hasn't landed yet — very first launch, offline install, etc.
resolveHost now prefers sharedIndex (which the poll keeps live) and, on a
miss with a stale index, triggers pollAndMerge (delta fetch) instead of
ensureIndex (full walk). The old full-walk fallback is only taken if the
delta primitives aren't available (running against an older resolver-web).
Argus/src/lib/resolver-web.js exports connectElectrum so the Theseus poll
can drive its own ad-hoc queries against the pool without duplicating the
Electrum wrapper.
Bundled starter snapshot refreshed: 73 beacon txs, root c37b859682…54e414ba.
Artifacts:
dist-public/TheseusNavigator-Setup-0.0.7.exe (95.4 MB)
sha256 1b0dabcd2b13067aa1fd89c3271708b35ba22dfc7a620e248b35e0487b7a104f
dist-public/TheseusNavigator-0.0.7-portable.exe (92.7 MB)
sha256 706dc01b21a88d3c2fbc3eeced0cab6ace873630bf3edc2d40915ca5be5c8cae
2026-08-30 15:57:01 +02:00
// Miss on a possibly-stale index → try a fast delta refresh (1 history +
// only-new-tx bodies), not a full walk. Only if we've had time for at least
// one poll to land (indexBuiltAt updated by both ensureIndex and the delta
// poll). If the delta path is unavailable (older resolver-web), fall back
// to a full rebuild — same behavior as before this change.
if ( ! entry && Date . now ( ) - indexBuiltAt > 8_000 ) {
const R = await getResolver ( ) ;
if ( R . connectElectrum && R . buildIndexFromSnapshot ) {
await pollAndMerge ( ) ;
entry = sharedIndex ? . get ( key ) ? ? null ;
} else {
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
Theseus: fix collision blank-page + broken-BCDN-open bugs
Two related bugs both caused by the tab's will-navigate handler racing with
programmatic loads:
Bug A (chip switch BCDN -> ICANN shows blank page, BCDN-priority mode):
fallbackToWeb() calls webContents.loadURL('https://<host>/') to serve the
ICANN version. That fired will-navigate, which saw a dotted host, ran
isBnsHost() -> true, and RE-INVOKED navigateTab() recursively — but the
transient='icann' override had already been consumed, so the recursive call
fell back to bcnr-first, canceling the fallback mid-flight. Tab showed blank
because both loads collided.
Fix: mark programmatic loads with t.internalNav so will-navigate skips them.
Bug B ('Ask each time' -> Open BCDN doesn't load):
Was going through a meta-refresh from bns://collision-choose/ to
bns://<host>/?_collision=bcnr. The meta-refresh bypassed navigateTab, so the
chrome/prov state was never updated (address bar, badges stayed stale).
Fix: will-navigate now catches bns://collision-choose/ FIRST, applies the
remember flag, sets t.collisionOverride, and routes via navigateTab so
chrome + prov update correctly.
Removed the serveBns collision-choose handler + the ?_collision URL-param
path in loadBns (both dead now that will-navigate handles it).
Result:
- Soft-mode 'Open with...' -> Open BCDN loads the BCDN site cleanly.
- Chip switcher flips BCDN <-> ICANN with no blank flash, correct chrome.
2026-08-02 15:21:21 +02:00
// (bns://collision-choose/ is handled by the will-navigate listener attached
// to each tab — it fires BEFORE the request reaches this protocol handler.)
2026-08-02 14:43:31 +02:00
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 ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
// Subdomain inheritance: `checkers.game.x` collapses to `game.x` in the
// registry (see resolver-web `normalizeName`). `ip` semantics apply to the
// whole namespace via Host routing; `s3` semantics are exact-key per name.
// For a subdomain, `ip` is the unambiguous parent intent — prefer it. For the
// apex (host === entry.name), current priority stands. See public-gateway.mjs
// for the full argument; keep this in step with that file.
const isSubdomain = host !== rec . entry . name ;
const serveIp = async ( ) => {
gateway+Theseus: pin BNS ip-record fetch to on-chain tls fingerprint
Uncovered by the 2026-08-13 subdomain-inheritance fix: once
`checkers.game.x` correctly picked the parent's `ip` record instead of
`s3`, the ip branch itself failed. Two reasons:
1. `fetch("http://<ip>/", { headers: { host: name } })` follows the
site's :80→:443 redirect into `https://<name>.<tld>/`, which isn't
in ICANN DNS → "fetch failed".
2. The site's cert is signed by a per-machine BNS root, not a public
CA; standard TLS validation rejects it.
Both are fixed by connecting to the IP with SNI = name, pinning the
presented cert's SHA-256 against the on-chain `tls` record, and only
then issuing the HTTPS request over the same socket. The on-chain
fingerprint is the trust anchor BNS uses everywhere else (see
Argus/src/lib/ca.js).
Gateway (public-gateway.mjs): new pinnedHttpsGet + httpGet + ipRequest
helpers; case "ip" delegates. No silent HTTP fallback on pin failure
(a mismatch means "not the site the chain says it is").
Theseus (main.js): parallel port of the same helpers, Tor-aware
(routes through SocksProxyAgent when Tor is on). serveBns's inner
serveIp() delegates to ipRequest.
Verified live: `curl -sI https://navigate.st/bns/checkers.game.x/`
returns 200 OK with the checkers game (1,179,215 bytes, apex
`game.x` unchanged, served from Sia).
2026-08-16 20:28:58 +02:00
// See ipRequest above: HTTPS-with-fingerprint-pin against the on-chain `tls`
// record when available, HTTP fallback when not. Fixes serving BNS names
// whose server redirects :80→:443 (the plain-fetch path chokes on the
// redirect target because it isn't in ICANN DNS).
const up = await ipRequest ( r . ip , reqPath + url . search , host , r . tls ) ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
return new Response ( up . buffer , { status : up . status , headers : { "content-type" : up . contentType || guessType ( reqPath ) } } ) ;
} ;
2026-07-29 13:54:34 +02:00
try {
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
if ( isSubdomain && r . ip ) return await serveIp ( ) ;
2026-07-29 13:54:34 +02:00
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 } } ) ;
}
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
if ( r . ip ) return await serveIp ( ) ;
2026-07-29 13:54:34 +02:00
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 ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
// Address-bar suggestions dropdown. Floating overlay under the address bar.
let addressPicker , apVisible = false , apPos = { x : 60 , y : 70 } ;
let apW = 520 ; let apH = 60 ;
// Password-fill picker — floating dropdown under a small key chip in the
// toolbar that appears only when the vault is unlocked AND the current
// site has matching credentials.
let pwFillPop , pwfVisible = false , pwfPos = { x : 8 , y : 90 } ;
const PWF _W = 280 ; let pwfH = 80 ;
2026-08-29 15:49:30 +02:00
// Link-hover status bar — small pill at the bottom-left of the window
// showing the href when the mouse hovers a link (Chrome / Firefox style).
// Hidden when hover leaves. Fed by webContents.update-target-url on every
// tab; the pill auto-sizes to its text.
let linkStatus , linkStatusVisible = false ;
let linkStatusW = 100 , linkStatusH = 22 ;
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
// Add-on sidebar — one right-anchored WebContentsView that hosts an add-on's
// registered panel HTML. First registered panel wins for the MVP; a tab
// strip / picker for multiple panels lands in a later rev. Sidebar loads
// nothing until the user actively opens it, so the perf cost of an unused
// add-on is nil.
let sidebar , sidebarVisible = false , sidebarActivePanelId = null ;
2026-08-31 16:00:34 +02:00
// Sidebar width is user-adjustable via a drag grip on the panel's left edge.
// The value below is the default; settings.sidebarWidth overrides it once
// loadSettings() runs and persists any drag adjustment made by the user.
const SIDEBAR _W _MIN = 200 , SIDEBAR _W _MAX = 800 , SIDEBAR _W _DEFAULT = 340 ;
let sidebarW = SIDEBAR _W _DEFAULT ;
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
// The add-on host is the single point of truth for what's installed and
// active. Populated by initAddons() at app-ready time.
let addonHost = null ;
const { AddonHost } = require ( "./addons-host.js" ) ;
function addonsUserDir ( ) { return path . join ( app . getPath ( "userData" ) , "addons" ) ; }
function addonsDataDir ( ) { return path . join ( app . getPath ( "userData" ) , "addons-data" ) ; }
function bundledAddonsDir ( ) { return path . join ( RES _DIR , "bundled-addons" ) ; }
// Copy bundled reference add-ons (shipped inside resources/) into the user's
// addons directory the first time we see them missing. Users can then edit,
// disable, or delete them — the framework treats bundled and user add-ons
// identically, no special path handling.
function seedBundledAddons ( ) {
const dst = addonsUserDir ( ) ;
try { fs . mkdirSync ( dst , { recursive : true } ) ; } catch { }
const src = bundledAddonsDir ( ) ;
if ( ! fs . existsSync ( src ) ) return ;
let entries = [ ] ;
try { entries = fs . readdirSync ( src , { withFileTypes : true } ) ; } catch { return ; }
for ( const e of entries ) {
if ( ! e . isDirectory ( ) ) continue ;
const target = path . join ( dst , e . name ) ;
if ( fs . existsSync ( target ) ) continue ; // never overwrite user copies
try { fs . cpSync ( path . join ( src , e . name ) , target , { recursive : true } ) ; }
catch ( err ) { console . warn ( ` [addons] seed ${ e . name } failed: ` , err ? . message ) ; }
}
}
function initAddons ( ) {
seedBundledAddons ( ) ;
addonHost = new AddonHost ( {
addonsDir : addonsUserDir ( ) ,
dataDir : addonsDataDir ( ) ,
isDisabled : ( id ) => Array . isArray ( settings . disabledAddons ) && settings . disabledAddons . includes ( id ) ,
logger : ( ... a ) => console . log ( "[addons]" , ... a ) ,
2026-08-31 16:00:34 +02:00
// Session-proxy capability. Add-ons that declare "session-proxy" in
// their manifest can call api.setSessionProxy(rules) to swap
// Chromium's outbound network path. Same primitive Tor uses.
setSessionProxy : async ( rules , addonId ) => {
const ses = session . defaultSession ;
if ( rules == null || rules === "" ) {
console . log ( ` [addons] [ ${ addonId } ] clearing session proxy ` ) ;
try { await ses . setProxy ( { proxyRules : "" } ) ; } catch ( e ) { console . warn ( "proxy clear failed:" , e ? . message ) ; }
return ;
}
const opts = typeof rules === "string" ? { proxyRules : rules } : rules ;
console . log ( ` [addons] [ ${ addonId } ] setting session proxy: ` , opts . proxyRules || JSON . stringify ( opts ) ) ;
try { await ses . setProxy ( opts ) ; } catch ( e ) { console . warn ( "proxy set failed:" , e ? . message ) ; }
} ,
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
} ) ;
addonHost . discoverAndActivate ( ) ;
const snap = addonHost . snapshot ( ) ;
console . log ( ` [addons] ${ snap . installed . length } installed, ${ snap . installed . filter ( ( x ) => x . enabled ) . length } enabled, ${ snap . sidebarPanels . length } sidebar panels ` ) ;
}
// Given a webContents sender URL, work out which add-on folder it lives in.
// Used to gate storage IPC — a page hosted inside addons/<id>/ can only touch
// its own store.
function addonIdForSender ( sender ) {
try {
const u = new URL ( sender . getURL ( ) ) ;
if ( u . protocol !== "file:" ) return null ;
const filePath = decodeURIComponent ( u . pathname ) . replace ( /^\/+/ , "" ) ;
const norm = filePath . replace ( /\\/g , "/" ) ;
const dirNorm = addonsUserDir ( ) . replace ( /\\/g , "/" ) . replace ( /\/+$/ , "" ) ;
if ( ! norm . toLowerCase ( ) . startsWith ( dirNorm . toLowerCase ( ) + "/" ) ) return null ;
const rest = norm . slice ( dirNorm . length + 1 ) ;
const first = rest . split ( "/" ) [ 0 ] ;
return first || null ;
} catch { return null ; }
}
2026-08-02 11:39:00 +02:00
// 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 ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
emitPwAvailability ( ) ;
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 ) ;
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
// Sidebar (when visible) claims a fixed slice on the right; the tab views
// shrink to fit alongside it. When hidden, tabs get the full width.
2026-08-31 16:00:34 +02:00
const sideW = sidebarVisible ? sidebarW : 0 ;
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
const tabW = Math . max ( 0 , width - sideW ) ;
for ( const t of tabs ) t . view . setBounds ( { x : 0 , y : CHROME _H , width : tabW , height : bodyH } ) ;
if ( sidebar ) sidebar . setBounds ( { x : tabW , y : CHROME _H , width : sideW , 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 ( ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
positionAddressPicker ( ) ;
positionPwFill ( ) ;
2026-08-29 15:49:30 +02:00
if ( linkStatusVisible ) positionLinkStatus ( ) ;
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 ; }
}
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
function positionAddressPicker ( ) {
if ( ! addressPicker ) return ;
const { width } = win . getContentBounds ( ) ;
const x = Math . max ( 6 , Math . min ( apPos . x , width - apW - 6 ) ) ;
addressPicker . setBounds ( { x , y : apPos . y , width : apW , height : apH } ) ;
}
function positionPwFill ( ) {
if ( ! pwFillPop ) return ;
const { width } = win . getContentBounds ( ) ;
const x = Math . max ( 6 , Math . min ( pwfPos . x , width - PWF _W - 6 ) ) ;
pwFillPop . setBounds ( { x , y : pwfPos . y , width : PWF _W , height : pwfH } ) ;
}
2026-08-29 15:49:30 +02:00
function positionLinkStatus ( ) {
if ( ! linkStatus || ! win ) return ;
const { width , height } = win . getContentBounds ( ) ;
const w = Math . min ( Math . max ( 120 , linkStatusW ) , Math . max ( 200 , width - 20 ) ) ;
const h = Math . max ( 20 , linkStatusH ) ;
linkStatus . setBounds ( { x : 0 , y : Math . max ( 0 , height - h ) , width : w , height : h } ) ;
}
function showLinkStatus ( url ) {
if ( ! linkStatus ) return ;
const s = String ( url || "" ) ;
if ( ! s ) {
if ( linkStatusVisible ) { linkStatus . setVisible ( false ) ; linkStatusVisible = false ; }
return ;
}
positionLinkStatus ( ) ;
// Raise the pill above any tab view that was added after it.
try { win . contentView . removeChildView ( linkStatus ) ; win . contentView . addChildView ( linkStatus ) ; } catch { }
linkStatus . setVisible ( true ) ; linkStatusVisible = true ;
try { linkStatus . webContents . send ( "link-status-url" , s ) ; } catch { }
}
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
// Open (or close) the sidebar. Loading the panel HTML is lazy — the first
// open triggers loadFile; subsequent opens just flip visibility.
function toggleSidebar ( ) { setSidebar ( ! sidebarVisible ) ; }
function setSidebar ( show , panelId ) {
if ( ! sidebar ) return ;
const panels = addonHost ? addonHost . getSidebarPanels ( ) : [ ] ;
if ( show && panels . length === 0 ) {
// No add-on offers a sidebar panel — silently ignore. Settings surfaces
// the "install one" path.
return ;
}
if ( show ) {
const wantId = panelId || sidebarActivePanelId || panels [ 0 ] . panelId ;
const panel = panels . find ( ( p ) => p . panelId === wantId ) || panels [ 0 ] ;
if ( sidebarActivePanelId !== panel . panelId ) {
sidebarActivePanelId = panel . panelId ;
try { sidebar . webContents . loadFile ( panel . pageFile ) ; } catch ( e ) { console . warn ( "sidebar loadFile failed:" , e ? . message ) ; }
}
sidebarVisible = true ;
sidebar . setVisible ( true ) ;
try { win . contentView . removeChildView ( sidebar ) ; win . contentView . addChildView ( sidebar ) ; } catch { }
layout ( ) ;
try { sidebar . webContents . send ( "sidebar-visibility" , true ) ; } catch { }
try { chrome ? . webContents . send ( "sidebar-state" , { visible : true , active : sidebarActivePanelId , panels } ) ; } catch { }
} else {
sidebarVisible = false ;
sidebar . setVisible ( false ) ;
layout ( ) ;
try { sidebar . webContents . send ( "sidebar-visibility" , false ) ; } catch { }
try { chrome ? . webContents . send ( "sidebar-state" , { visible : false , active : sidebarActivePanelId , panels } ) ; } catch { }
}
}
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
function showPwFill ( show , matches ) {
if ( ! pwFillPop ) return ;
if ( show ) {
positionPwFill ( ) ;
win . contentView . removeChildView ( pwFillPop ) ;
win . contentView . addChildView ( pwFillPop ) ;
pwFillPop . setVisible ( true ) ; pwfVisible = true ;
pwFillPop . webContents . send ( "pw-matches" , { matches : matches || [ ] } ) ;
} else { pwFillPop . setVisible ( false ) ; pwfVisible = false ; }
}
// Compute credential matches for a host. Exact hostname match in phase-1;
// eTLD+1 upgrade queued for A.2.5 (needs the public-suffix-list snapshot).
function pwMatchesForHost ( host ) {
if ( ! vaultState || ! host ) return [ ] ;
const h = String ( host ) . toLowerCase ( ) ;
return ( vaultState . entries || [ ] )
. filter ( ( e ) => e . domain === h )
. map ( ( e ) => ( { id : e . id , domain : e . domain , username : e . username || "" } ) ) ;
}
// Emit the current tab's match count to chrome so the toolbar chip can
// show/hide + display the count. Cheap; called on nav + vault unlock/lock.
function emitPwAvailability ( ) {
const t = activeTab ( ) ;
const host = t ? . prov ? . host || "" ;
const count = pwMatchesForHost ( host ) . length ;
try { chrome ? . webContents . send ( "pw-availability" , { host , count } ) ; } catch { }
}
// Inject a small script into the active tab that fills the first visible
// password field + tries to fill the adjacent/associated username field.
// Kept intentionally small — the whole autofill affordance is opt-in
// (user clicks the chip; nothing runs on page load).
async function pwFillIntoActiveTab ( entry ) {
const t = activeTab ( ) ; if ( ! t ) return false ;
const wc = t . view . webContents ;
const script = ` (() => {
const visible = ( el ) => { const r = el . getBoundingClientRect ( ) ; return r . width > 4 && r . height > 4 ; } ;
const pwds = [ ... document . querySelectorAll ( 'input[type=password]:not([disabled])' ) ] . filter ( visible ) ;
if ( ! pwds . length ) return { ok : false , why : 'no-password-field' } ;
const pw = pwds [ 0 ] ;
const form = pw . closest ( 'form' ) ;
const scope = form ? form . querySelectorAll ( 'input' ) : document . querySelectorAll ( 'input' ) ;
const users = [ ... scope ] . filter ( ( el ) => el !== pw && visible ( el ) && ! el . disabled &&
/^(?:text|email|tel|url|search|)$/i . test ( el . type || 'text' ) &&
/^(?:username|user|email|login|account|id)$/i . test ( ( el . name || el . id || el . autocomplete || '' ) . replace ( /[-_]/g , '' ) . toLowerCase ( ) ) ) ;
const user = users [ 0 ] || null ;
const fill = ( el , v ) => {
el . focus ( ) ;
const setter = Object . getOwnPropertyDescriptor ( HTMLInputElement . prototype , 'value' ) . set ;
setter . call ( el , v ) ;
el . dispatchEvent ( new Event ( 'input' , { bubbles : true } ) ) ;
el . dispatchEvent ( new Event ( 'change' , { bubbles : true } ) ) ;
} ;
if ( user && $ { JSON . stringify ( String ( entry . username || "" ) ) } ) fill ( user , $ { JSON . stringify ( String ( entry . username || "" ) ) } ) ;
fill ( pw , $ { JSON . stringify ( String ( entry . password ) ) } ) ;
pw . blur ( ) ;
return { ok : true , filledUsername : ! ! user } ;
} ) ( ) ` ;
try {
const res = await wc . executeJavaScript ( script , true ) ;
return res ;
} catch ( e ) { console . error ( "pw fill failed:" , e ? . message ) ; return { ok : false , why : "exec-error" } ; }
}
function showAddressPicker ( show , suggestions ) {
if ( ! addressPicker ) return ;
if ( show ) {
if ( ! suggestions || ! suggestions . length ) return showAddressPicker ( false ) ;
positionAddressPicker ( ) ;
win . contentView . removeChildView ( addressPicker ) ;
win . contentView . addChildView ( addressPicker ) ;
addressPicker . setVisible ( true ) ; apVisible = true ;
addressPicker . webContents . send ( "address-suggest" , { suggestions } ) ;
} else { addressPicker . setVisible ( false ) ; apVisible = false ; }
}
2026-08-02 11:39:00 +02:00
// 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-08-29 15:49:30 +02:00
if ( linkStatusVisible ) showLinkStatus ( "" ) ; // clear any lingering hover pill
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-08-29 23:29:18 +02:00
tabs : tabs . map ( ( x ) => ( { id : x . id , title : x . title || "New Tab" , active : x . id === activeId , loading : ! ! x . loading , favicon : x . favicon || null } ) ) ,
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-08-06 01:41:31 +02:00
// Pull the tab's real URL from webContents after Electron navigates, so in-page
// clicks (subpages of a BCNR site, subdomain hops, cross-origin redirects) update
// the address bar. Without this, t.url is only refreshed on programmatic loads —
// navigateTab / the collision switcher — and everything else sticks on the parent.
// Internal bns:// → https:// for display, matching navigateTab's convention that
// https:// is what the user sees regardless of how the bytes were fetched.
function refreshTabUrl ( tab ) {
if ( ! tab || tab . prov ? . kind === "home" ) return ; // home is loadFile → file://; leave t.url = ""
try {
const raw = tab . view . webContents . getURL ( ) ;
if ( raw && ! raw . startsWith ( "file:" ) ) tab . url = raw . replace ( /^bns:\/\// , "https://" ) ;
} catch { }
}
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 ( ) ;
}
Theseus 0.1.3: branded error page for load failures (BUILT, NOT DEPLOYED)
Setup f2afc14efc63008cbb9dad44176e94146386db4c0afda4459f1d4eb929172b6d
Portable 5d08b1415526934db8de780949a610896064fe9567aa0e5e1702ebabd7eb7df2
Chromium's default 'This site can't be reached' replaced with a Theseus-
themed error page. did-fail-load on every tab's webContents (main frame
only, non-ignorable code) routes the tab to error.html with the
attempt URL, host, error code, and description as query params. The
page keeps t.url pointing at the failed URL so the address bar shows
what the user typed and they can edit + retry - refreshTabUrl's
existing file:// skip means the error page's own path never leaks
back into the bar.
Five kinds, chosen by pickErrorKind(code, host):
name-not-registered BCNR-eligible host + ERR_NAME_NOT_RESOLVED.
Says "no BCDN record on chain, no clearnet host
either." Offers Register on Sirius + Search +
Retry + Home.
name-unreachable ERR_NAME_NOT_RESOLVED on a non-BCNR host. DNS
failed - offers Retry + Search + Register +
Home.
unreachable CONN_REFUSED/RESET/TIMED_OUT/CLOSED/NETWORK_CHANGED.
Offers Retry + Tor guide + Home.
tls ERR_CERT_* range (-200..-299). Offers Retry +
Home.
generic Everything else.
home-preload.js gains `window.errorpage` alongside `window.home`. Both
APIs are sender-URL-gated in main - a random page seeing the shape
can't invoke them (isErrorPageSender / isHomePageSender). The external-
open handler additionally allowlists Silent Mode domains only.
package.json build.files gets error.html + error-preload.js so
electron-builder actually bundles them (GOTCHAS rule: an unlisted
runtime-loaded file silently opens blank).
Ship pages (releases-manifest.json, tools/index.html, releases/index.html,
site-theseus-x/index.html) updated to 0.1.3 with the new hashes.
DEPLOY STATUS - blocked on VPS SSH: my IP was hit with a full-port ban
mid-turn (likely fail2ban from the burst of scp during the 0.1.0-0.1.2
iterations). Site pages/manifest/installers are committed locally but
NOT yet on dl.silentmode.st or the Sia mirror. Live still reads 0.1.2.
User needs to unban 195.184.247.106 on their end, or wait for the ban
to expire, before the ship pages match reality.
2026-08-31 13:38:05 +02:00
// Errors we deliberately ignore (Chromium's own reasons that shouldn't show
// a user-facing error page):
// -3 ERR_ABORTED — navigation superseded by another / user pressed Stop
// -20 ERR_BLOCKED_BY_CLIENT — extension/ad-blocker style cancel
const ERROR _CODE _IGNORE = new Set ( [ - 3 , - 20 ] ) ;
// Chromium error-code buckets. Keep the ranges narrow — anything unmapped
// falls through to the generic error page.
// -105 ERR_NAME_NOT_RESOLVED
// -102 ERR_CONNECTION_REFUSED
// -101 ERR_CONNECTION_RESET
// -118 ERR_CONNECTION_TIMED_OUT
// -100 ERR_CONNECTION_CLOSED
// -7 ERR_TIMED_OUT
// -21 ERR_NETWORK_CHANGED
const ERROR _UNREACHABLE = new Set ( [ - 102 , - 101 , - 118 , - 100 , - 7 , - 21 ] ) ;
function pickErrorKind ( code , host ) {
if ( code === - 105 ) {
// Name didn't resolve. If the host is BCNR-eligible (has a real TLD),
// that also means BCNR had no record — otherwise resolveHost/loadBns
// would have served something. Treat as "not registered" to promote
// the register-on-Sirius action.
return isBnsHost ( host ) ? "name-not-registered" : "name-unreachable" ;
}
if ( ERROR _UNREACHABLE . has ( code ) ) return "unreachable" ;
if ( code <= - 200 && code >= - 299 ) return "tls" ; // ERR_CERT_* range
return "generic" ;
}
// Load the branded error surface for a failed navigation. Keeps t.url =
// the attempted URL so the address bar still shows what the user asked
// for and they can edit + retry; refreshTabUrl already skips file:// so
// the error page's own path never leaks back into the bar.
function loadErrorPage ( t , id , { url , code , desc } ) {
if ( ! t ) return ;
const failedUrl = String ( url || t . url || "" ) ;
let host = "" ;
try { host = new URL ( failedUrl ) . hostname ; } catch { }
const kind = pickErrorKind ( code , host ) ;
const q = new URLSearchParams ( {
kind , host , url : failedUrl ,
code : String ( code || "" ) , desc : String ( desc || "" ) ,
} ) . toString ( ) ;
t . internalNav = true ;
t . title = host ? "Error — " + host : "Load error" ;
t . prov = { host , kind : "error" , code , desc } ;
t . view . webContents . loadFile ( path . join ( _ _dirname , "error.html" ) , { search : q } )
. catch ( ( e ) => console . warn ( "error page load failed:" , e ? . message ) )
. finally ( ( ) => { t . internalNav = false ; } ) ;
if ( id === activeId ) pushNav ( t . prov ) ;
emitTabs ( ) ;
}
2026-07-29 13:54:34 +02:00
function createTab ( initial , opts = { } ) {
const id = ++ tabSeq ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
// Non-settings tabs get home-preload so the built-in home page can round-
// trip its editable-cards state via IPC. IPC handlers reject any call
// whose sender URL isn't our own home.html, so a third-party page sees
// the API's shape but can't act through it.
const view = new WebContentsView ( opts . settings
? { webPreferences : { preload : path . join ( _ _dirname , "settings-preload.js" ) } }
: { webPreferences : { preload : path . join ( _ _dirname , "home-preload.js" ) } } ) ;
2026-07-29 13:54:34 +02:00
const wc = view . webContents ;
try { wc . setWebRTCIPHandlingPolicy ( webrtcPolicy ( ) ) ; } catch { }
try { wc . setBackgroundThrottling ( settings . backgroundThrottle ) ; } catch { }
applyFingerprint ( wc ) ;
2026-08-29 23:29:18 +02:00
const tab = { id , view , title : opts . settings ? "Settings" : "New Tab" , url : "" , favicon : null , prov : null , settings : ! ! opts . settings } ;
2026-07-29 13:54:34 +02:00
tabs . push ( tab ) ;
win . contentView . addChildView ( view ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
wc . on ( "page-title-updated" , ( _e , title ) => {
tab . title = title ; emitTabs ( ) ;
// Keep the top-of-history title in sync when a page's title loads late.
if ( history [ 0 ] && tab . url && history [ 0 ] . url === tab . url ) { history [ 0 ] . title = title ; saveHistoryDebounced ( ) ; }
} ) ;
2026-08-29 23:29:18 +02:00
// Site favicon → tab icon. Take the first URL Electron emits (usually the
// 32x32 or 16x16 <link rel="icon">). We don't proactively clear on nav —
// mainstream browsers keep the old icon until the new one arrives, which
// avoids a flash on every subpage click.
wc . on ( "page-favicon-updated" , ( _e , urls ) => {
const next = ( urls && urls [ 0 ] ) || null ;
if ( tab . favicon !== next ) { tab . favicon = next ; emitTabs ( ) ; }
} ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
wc . on ( "did-navigate" , ( ) => { refreshTabUrl ( tab ) ; emitTabs ( ) ; historyAdd ( tab . url , tab . title ) ; } ) ;
wc . on ( "did-navigate-in-page" , ( ) => { refreshTabUrl ( tab ) ; emitTabs ( ) ; historyAdd ( tab . url , tab . title ) ; } ) ;
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 ) ) ;
Theseus 0.1.3: branded error page for load failures (BUILT, NOT DEPLOYED)
Setup f2afc14efc63008cbb9dad44176e94146386db4c0afda4459f1d4eb929172b6d
Portable 5d08b1415526934db8de780949a610896064fe9567aa0e5e1702ebabd7eb7df2
Chromium's default 'This site can't be reached' replaced with a Theseus-
themed error page. did-fail-load on every tab's webContents (main frame
only, non-ignorable code) routes the tab to error.html with the
attempt URL, host, error code, and description as query params. The
page keeps t.url pointing at the failed URL so the address bar shows
what the user typed and they can edit + retry - refreshTabUrl's
existing file:// skip means the error page's own path never leaks
back into the bar.
Five kinds, chosen by pickErrorKind(code, host):
name-not-registered BCNR-eligible host + ERR_NAME_NOT_RESOLVED.
Says "no BCDN record on chain, no clearnet host
either." Offers Register on Sirius + Search +
Retry + Home.
name-unreachable ERR_NAME_NOT_RESOLVED on a non-BCNR host. DNS
failed - offers Retry + Search + Register +
Home.
unreachable CONN_REFUSED/RESET/TIMED_OUT/CLOSED/NETWORK_CHANGED.
Offers Retry + Tor guide + Home.
tls ERR_CERT_* range (-200..-299). Offers Retry +
Home.
generic Everything else.
home-preload.js gains `window.errorpage` alongside `window.home`. Both
APIs are sender-URL-gated in main - a random page seeing the shape
can't invoke them (isErrorPageSender / isHomePageSender). The external-
open handler additionally allowlists Silent Mode domains only.
package.json build.files gets error.html + error-preload.js so
electron-builder actually bundles them (GOTCHAS rule: an unlisted
runtime-loaded file silently opens blank).
Ship pages (releases-manifest.json, tools/index.html, releases/index.html,
site-theseus-x/index.html) updated to 0.1.3 with the new hashes.
DEPLOY STATUS - blocked on VPS SSH: my IP was hit with a full-port ban
mid-turn (likely fail2ban from the burst of scp during the 0.1.0-0.1.2
iterations). Site pages/manifest/installers are committed locally but
NOT yet on dl.silentmode.st or the Sia mirror. Live still reads 0.1.2.
User needs to unban 195.184.247.106 on their end, or wait for the ban
to expire, before the ship pages match reality.
2026-08-31 13:38:05 +02:00
// Failed loads: NAME_NOT_RESOLVED, CONNECTION_REFUSED, cert errors, etc.
// Show the branded error page instead of Chromium's default "This site
// can't be reached". Skip subframe errors, our own programmatic loads,
// and the couple of Chromium codes that fire on normal user actions
// (Stop / superseded nav / extension cancel).
wc . on ( "did-fail-load" , ( _e , code , desc , validatedURL , isMainFrame ) => {
if ( ! isMainFrame ) return ;
if ( tab . internalNav ) return ;
if ( ERROR _CODE _IGNORE . has ( code ) ) return ;
loadErrorPage ( tab , tab . id , { url : validatedURL || tab . url , code , desc } ) ;
} ) ;
2026-08-29 15:49:30 +02:00
// Firefox / Chrome-style bottom-left link preview: fires with the href
// when the pointer enters/leaves an anchor. Empty string = no hover.
wc . on ( "update-target-url" , ( _e , url ) => { if ( tab . id === activeId ) showLinkStatus ( url ) ; } ) ;
2026-07-29 13:54:34 +02:00
wc . on ( "will-navigate" , ( e , u ) => {
try {
Theseus: fix collision blank-page + broken-BCDN-open bugs
Two related bugs both caused by the tab's will-navigate handler racing with
programmatic loads:
Bug A (chip switch BCDN -> ICANN shows blank page, BCDN-priority mode):
fallbackToWeb() calls webContents.loadURL('https://<host>/') to serve the
ICANN version. That fired will-navigate, which saw a dotted host, ran
isBnsHost() -> true, and RE-INVOKED navigateTab() recursively — but the
transient='icann' override had already been consumed, so the recursive call
fell back to bcnr-first, canceling the fallback mid-flight. Tab showed blank
because both loads collided.
Fix: mark programmatic loads with t.internalNav so will-navigate skips them.
Bug B ('Ask each time' -> Open BCDN doesn't load):
Was going through a meta-refresh from bns://collision-choose/ to
bns://<host>/?_collision=bcnr. The meta-refresh bypassed navigateTab, so the
chrome/prov state was never updated (address bar, badges stayed stale).
Fix: will-navigate now catches bns://collision-choose/ FIRST, applies the
remember flag, sets t.collisionOverride, and routes via navigateTab so
chrome + prov update correctly.
Removed the serveBns collision-choose handler + the ?_collision URL-param
path in loadBns (both dead now that will-navigate handles it).
Result:
- Soft-mode 'Open with...' -> Open BCDN loads the BCDN site cleanly.
- Chip switcher flips BCDN <-> ICANN with no blank flash, correct chrome.
2026-08-02 15:21:21 +02:00
// Skip our own programmatic loads. fallbackToWeb calls loadURL("https://<host>/")
// and that host is often a BCNR-registered dotted name — without this guard,
// isBnsHost() would send us right back into navigateTab, canceling the
// fallback (blank-page bug 2026-08-02).
if ( tab . internalNav ) return ;
2026-07-29 13:54:34 +02:00
const parsed = new URL ( u ) ;
Theseus: fix collision blank-page + broken-BCDN-open bugs
Two related bugs both caused by the tab's will-navigate handler racing with
programmatic loads:
Bug A (chip switch BCDN -> ICANN shows blank page, BCDN-priority mode):
fallbackToWeb() calls webContents.loadURL('https://<host>/') to serve the
ICANN version. That fired will-navigate, which saw a dotted host, ran
isBnsHost() -> true, and RE-INVOKED navigateTab() recursively — but the
transient='icann' override had already been consumed, so the recursive call
fell back to bcnr-first, canceling the fallback mid-flight. Tab showed blank
because both loads collided.
Fix: mark programmatic loads with t.internalNav so will-navigate skips them.
Bug B ('Ask each time' -> Open BCDN doesn't load):
Was going through a meta-refresh from bns://collision-choose/ to
bns://<host>/?_collision=bcnr. The meta-refresh bypassed navigateTab, so the
chrome/prov state was never updated (address bar, badges stayed stale).
Fix: will-navigate now catches bns://collision-choose/ FIRST, applies the
remember flag, sets t.collisionOverride, and routes via navigateTab so
chrome + prov update correctly.
Removed the serveBns collision-choose handler + the ?_collision URL-param
path in loadBns (both dead now that will-navigate handles it).
Result:
- Soft-mode 'Open with...' -> Open BCDN loads the BCDN site cleanly.
- Chip switcher flips BCDN <-> ICANN with no blank flash, correct chrome.
2026-08-02 15:21:21 +02:00
// Intercept the collision-choose posted by the in-tab "Open with…" page,
// apply the remember flag, set a one-shot transient override so loadBns
// doesn't re-prompt, and route via navigateTab so chrome/prov stay in sync.
if ( parsed . protocol === "bns:" && parsed . hostname === "collision-choose" ) {
e . preventDefault ( ) ;
const p = parsed . 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 ( ! target ) return ;
if ( cChoice === "bcnr" || cChoice === "icann" ) {
rememberCollision ( target , cTld , cChoice , cRem ) ;
tab . collisionOverride = cChoice ; // one-shot, consumed by loadBns
}
return navigateTab ( id , target + rest ) ;
}
2026-07-29 13:54:34 +02:00
if ( parsed . protocol === "bns:" ) return ;
Theseus: preserve query on link-navigation intercept + immediate address-bar reflect
Two related navigation bugs, both surfacing when a page inside a tab
tries to submit a search:
1. will-navigate rewriter dropped the query string and fragment.
Every dotted host went through
navigateTab(id, parsed.hostname + parsed.pathname)
which stripped ?q=... The classic-form-GET search engines
(Google, Brave, Bing, Startpage, Yandex, Ecosia, Mojeek, ...) all
silently landed on their /search endpoint with no query, so no
results ever showed. DuckDuckGo only appeared to work because its
in-page search uses history.pushState + XHR and never triggered
will-navigate to begin with.
Fix: pass the whole URL (minus scheme) so query + fragment survive.
2. Address bar showed the previous page's URL until the new page's
did-navigate fired. navigateTab called setLoading() -> emitTabs()
BEFORE assigning t.url, so the chrome renderer received the stale
URL and painted it (goURL() blurs the input on Enter, so the
"don't clobber typed text" guard didn't skip the write).
Fix: assign t.url from host+rest immediately, before setLoading.
2026-08-29 22:22:47 +02:00
if ( isBnsHost ( parsed . hostname ) ) {
2026-08-31 05:13:20 +02:00
// Only intercept cross-origin navigations. Same-origin (a form submit
// or a subpage link on the site we're currently on) must go through
// Chromium natively — our navigateTab path calls loadURL(url), which
// is always a GET and drops any POST body. That silently broke
// Startpage (whose in-page search form POSTs to /do/search), and any
// other site that POSTs (logins, comment submits, checkouts, ...).
// The site is already loaded from clearnet, so its subsequent
// navigation belongs to clearnet too — no BCNR re-lookup needed.
let currentHost = "" ;
try { currentHost = new URL ( wc . getURL ( ) ) . hostname ; } catch { }
if ( currentHost === parsed . hostname ) return ;
Theseus: preserve query on link-navigation intercept + immediate address-bar reflect
Two related navigation bugs, both surfacing when a page inside a tab
tries to submit a search:
1. will-navigate rewriter dropped the query string and fragment.
Every dotted host went through
navigateTab(id, parsed.hostname + parsed.pathname)
which stripped ?q=... The classic-form-GET search engines
(Google, Brave, Bing, Startpage, Yandex, Ecosia, Mojeek, ...) all
silently landed on their /search endpoint with no query, so no
results ever showed. DuckDuckGo only appeared to work because its
in-page search uses history.pushState + XHR and never triggered
will-navigate to begin with.
Fix: pass the whole URL (minus scheme) so query + fragment survive.
2. Address bar showed the previous page's URL until the new page's
did-navigate fired. navigateTab called setLoading() -> emitTabs()
BEFORE assigning t.url, so the chrome renderer received the stale
URL and painted it (goURL() blurs the input on Enter, so the
"don't clobber typed text" guard didn't skip the write).
Fix: assign t.url from host+rest immediately, before setLoading.
2026-08-29 22:22:47 +02:00
// Preserve query + fragment. Dropping them broke every search engine
// that submits via a classic form GET (Google's /search?q=foo lost
2026-08-31 05:13:20 +02:00
// the ?q=, so the results page opened blank).
Theseus: preserve query on link-navigation intercept + immediate address-bar reflect
Two related navigation bugs, both surfacing when a page inside a tab
tries to submit a search:
1. will-navigate rewriter dropped the query string and fragment.
Every dotted host went through
navigateTab(id, parsed.hostname + parsed.pathname)
which stripped ?q=... The classic-form-GET search engines
(Google, Brave, Bing, Startpage, Yandex, Ecosia, Mojeek, ...) all
silently landed on their /search endpoint with no query, so no
results ever showed. DuckDuckGo only appeared to work because its
in-page search uses history.pushState + XHR and never triggered
will-navigate to begin with.
Fix: pass the whole URL (minus scheme) so query + fragment survive.
2. Address bar showed the previous page's URL until the new page's
did-navigate fired. navigateTab called setLoading() -> emitTabs()
BEFORE assigning t.url, so the chrome renderer received the stale
URL and painted it (goURL() blurs the input on Enter, so the
"don't clobber typed text" guard didn't skip the write).
Fix: assign t.url from host+rest immediately, before setLoading.
2026-08-29 22:22:47 +02:00
e . preventDefault ( ) ;
navigateTab ( id , u . replace ( /^[a-z]+:\/\//i , "" ) ) ;
}
2026-07-29 13:54:34 +02:00
} catch { }
} ) ;
2026-08-02 18:50:58 +02:00
// "You have unsaved changes" confirmation: fires when the page's beforeunload
// handler is trying to keep the user on the page (e.g. an unsent form draft,
// an editor with a dirty document). Show a native confirm; on "Leave", call
// preventDefault to override the block. Applies to both link clicks AND our
// programmatic loads (chip switcher, address-bar navigation).
wc . on ( "will-prevent-unload" , ( e ) => {
const parent = BrowserWindow . getFocusedWindow ( ) || win ;
const choice = dialog . showMessageBoxSync ( parent , {
type : "question" ,
buttons : [ "Stay on page" , "Leave anyway" ] ,
defaultId : 0 ,
cancelId : 0 ,
title : "Unsaved changes" ,
message : "This page is asking you to stay." ,
detail : "You may have unsaved changes that will be lost if you leave." ,
} ) ;
if ( choice === 1 ) e . preventDefault ( ) ; // Leave anyway -> override the beforeunload
} ) ;
2026-07-29 13:54:34 +02:00
// 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" } ,
) ;
}
2026-08-29 23:25:22 +02:00
// Image context menu: only when the pointer is actually on an image, and
// we have a src to act on. Save-image-as triggers will-download with no
// preset savePath, so Electron shows the native Save As dialog.
if ( p . mediaType === "image" && p . srcURL ) {
items . push (
{ label : "Open image in new tab" , click : ( ) => createTab ( p . srcURL ) } ,
{ label : "Save image as…" , click : ( ) => wc . downloadURL ( p . srcURL ) } ,
{ label : "Copy image" , click : ( ) => { try { wc . copyImageAt ( p . x , p . y ) ; } catch { } } } ,
{ label : "Copy image address" , click : ( ) => clipboard . writeText ( p . srcURL ) } ,
{ type : "separator" } ,
) ;
}
2026-07-29 13:54:34 +02:00
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 ( ) {
2026-08-31 16:32:16 +02:00
win = new BrowserWindow ( {
width : 1220 , height : 840 , title : "Theseus Navigator" , backgroundColor : "#0f1420" ,
// Taskbar / titlebar icon. Packaged builds ship build/icon.ico as
// extraResource; dev reads the source file directly.
icon : app . isPackaged
? path . join ( process . resourcesPath , "icon.ico" )
: path . join ( _ _dirname , "build" , "icon.ico" ) ,
} ) ;
2026-07-29 13:54:34 +02:00
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 ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
// Floating address-bar suggestions dropdown.
addressPicker = new WebContentsView ( { webPreferences : { preload : path . join ( _ _dirname , "address-picker-preload.js" ) } } ) ;
try { addressPicker . setBackgroundColor ( "#00000000" ) ; } catch { }
win . contentView . addChildView ( addressPicker ) ;
addressPicker . webContents . loadFile ( "address-picker.html" ) ;
addressPicker . setVisible ( false ) ;
// Floating password-fill picker.
pwFillPop = new WebContentsView ( { webPreferences : { preload : path . join ( _ _dirname , "pw-fill-preload.js" ) } } ) ;
try { pwFillPop . setBackgroundColor ( "#00000000" ) ; } catch { }
win . contentView . addChildView ( pwFillPop ) ;
pwFillPop . webContents . loadFile ( "pw-fill.html" ) ;
pwFillPop . setVisible ( false ) ;
2026-08-29 15:49:30 +02:00
// Link-hover status pill (bottom-left of window).
linkStatus = new WebContentsView ( { webPreferences : { preload : path . join ( _ _dirname , "link-status-preload.js" ) } } ) ;
try { linkStatus . setBackgroundColor ( "#00000000" ) ; } catch { }
win . contentView . addChildView ( linkStatus ) ;
linkStatus . webContents . loadFile ( "link-status.html" ) ;
linkStatus . setVisible ( false ) ;
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
// Add-on sidebar host. Doesn't loadFile until an add-on panel is opened.
sidebar = new WebContentsView ( { webPreferences : { preload : path . join ( _ _dirname , "sidebar-preload.js" ) } } ) ;
win . contentView . addChildView ( sidebar ) ;
sidebar . 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 ( ) ;
Theseus 0.0.4: cheap in-app update-check chip
Bumps version so the chip actually surfaces itself on 0.0.3 installs
(the version-newer check requires a strict semver bump — same-version
rebuilds don't trigger the chip). From this release on, whenever the
manifest names a newer Theseus, users get a one-click download.
Mechanism
- main.js checkForUpdate() fetches https://dl.silentmode.st/releases-
manifest.json on startup (5s timeout, cache: no-store) + every 6h.
Finds the theseus-navigator release, compares version to
app.getVersion() with a numeric a.b.c comparator that handles
"0.10.0 > 0.9.9" correctly.
- On a match → stores { version, setupUrl, portableUrl, setupHash,
portableHash, date } and emits update-available to chrome. Cleared
after the user upgrades + relaunches (same-version → null).
- Re-emits on chrome's did-finish-load in case the fetch beats the
chrome view.
Chip UI (chrome.html)
- Acid-yellow pill between the downloads button and the Tor toggle:
"↓ Update to X.Y.Z" + a ✕. Main body opens setupUrl in the system
browser via shell.openExternal (origin-validated to
https://dl.silentmode.st/ or https://silentmode.st/). ✕ dismisses
for the current session — you'll see it again next launch if still
behind.
Trust anchor
- No signing / no cryptographic verification of the download in this
phase. releases.silentmode.bch publishes the SAME manifest URL, so
users who want to verify can cross-check the manifest hash against
what BCNR returns. The proper auto-updater with signature checks is
the follow-on to this cheap version.
Non-goals in phase 1
- No delta downloads; the user clicks and gets a full installer.
- No auto-install; download → user runs the installer themselves.
- No "check now" button in Settings; the periodic timer suffices.
- No portable-vs-installed detection; the chip prefers setupUrl (the
installer upgrades in place). Right-click for portable is future work.
2026-08-28 20:21:44 +02:00
// Re-emit any pending update notice — the fetch may have completed
// before chrome finished loading, in which case the initial send was
// a no-op.
emitUpdateAvailable ( ) ;
2026-07-29 13:54:34 +02:00
} ) ;
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 ;
// 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 ) || "/" ;
Theseus: preserve query on link-navigation intercept + immediate address-bar reflect
Two related navigation bugs, both surfacing when a page inside a tab
tries to submit a search:
1. will-navigate rewriter dropped the query string and fragment.
Every dotted host went through
navigateTab(id, parsed.hostname + parsed.pathname)
which stripped ?q=... The classic-form-GET search engines
(Google, Brave, Bing, Startpage, Yandex, Ecosia, Mojeek, ...) all
silently landed on their /search endpoint with no query, so no
results ever showed. DuckDuckGo only appeared to work because its
in-page search uses history.pushState + XHR and never triggered
will-navigate to begin with.
Fix: pass the whole URL (minus scheme) so query + fragment survive.
2. Address bar showed the previous page's URL until the new page's
did-navigate fired. navigateTab called setLoading() -> emitTabs()
BEFORE assigning t.url, so the chrome renderer received the stale
URL and painted it (goURL() blurs the input on Enter, so the
"don't clobber typed text" guard didn't skip the write).
Fix: assign t.url from host+rest immediately, before setLoading.
2026-08-29 22:22:47 +02:00
// Reflect the target URL immediately so the address bar doesn't keep
// showing the previous page's URL for the whole load duration. Without
// this, emitTabs below (triggered by setLoading) carries the old t.url
// and the chrome renderer paints it, since we blurred the input on Enter.
t . url = "https://" + host + ( rest === "/" ? "" : rest ) ;
setLoading ( t , true ) ; // show the loading indicator immediately (covers BNS resolution)
2026-07-29 13:54:34 +02:00
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 ) ;
Theseus: fix collision blank-page + broken-BCDN-open bugs
Two related bugs both caused by the tab's will-navigate handler racing with
programmatic loads:
Bug A (chip switch BCDN -> ICANN shows blank page, BCDN-priority mode):
fallbackToWeb() calls webContents.loadURL('https://<host>/') to serve the
ICANN version. That fired will-navigate, which saw a dotted host, ran
isBnsHost() -> true, and RE-INVOKED navigateTab() recursively — but the
transient='icann' override had already been consumed, so the recursive call
fell back to bcnr-first, canceling the fallback mid-flight. Tab showed blank
because both loads collided.
Fix: mark programmatic loads with t.internalNav so will-navigate skips them.
Bug B ('Ask each time' -> Open BCDN doesn't load):
Was going through a meta-refresh from bns://collision-choose/ to
bns://<host>/?_collision=bcnr. The meta-refresh bypassed navigateTab, so the
chrome/prov state was never updated (address bar, badges stayed stale).
Fix: will-navigate now catches bns://collision-choose/ FIRST, applies the
remember flag, sets t.collisionOverride, and routes via navigateTab so
chrome + prov update correctly.
Removed the serveBns collision-choose handler + the ?_collision URL-param
path in loadBns (both dead now that will-navigate handles it).
Result:
- Soft-mode 'Open with...' -> Open BCDN loads the BCDN site cleanly.
- Chip switcher flips BCDN <-> ICANN with no blank flash, correct chrome.
2026-08-02 15:21:21 +02:00
t . internalNav = true ;
try { await t . view . webContents . loadURL ( t . url ) ; }
catch ( e ) { console . warn ( "fallback loadURL failed:" , e ? . message ) ; }
finally { t . internalNav = false ; }
2026-07-30 22:09:26 +02:00
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-08-02 19:26:47 +02:00
// Address-bar display: show https:// even for BCDN sites. Rationale — BCNR
// replaces DNS (name resolution), NOT HTTP (transport). For s3/ip/p records
// the actual delivery IS HTTPS under the hood; for h records the content is
// on-chain (no HTTP at all, but https:// is the least surprising display).
// The BCDN/ICANN badge is the source-of-truth for which registry served us;
// the URL scheme is a convention, kept consistent so users' muscle memory
// holds. (bns:// is an internal Electron protocol implementation detail.)
t . url = "https://" + 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 } ` ) ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
// Source badge must mirror what serveBns actually picks — subdomain-with-ip
// routes via the parent's server, not via Sia. See serveBns for the rule.
const _isSub = host !== entry . name ;
const src = ( _isSub && entry . records . ip ) ? "direct server"
: entry . records . h ? "on-chain (chain)"
: entry . records . s3 ? "Sia network"
: entry . records . ip ? "direct server"
: entry . records . u ? "redirect"
: "record" ;
2026-07-29 13:54:34 +02:00
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 ) ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
ipcMain . handle ( "move-tab" , ( _e , id , targetId , place ) => {
const src = tabs . findIndex ( ( t ) => t . id === id ) ;
const dst = tabs . findIndex ( ( t ) => t . id === targetId ) ;
if ( src < 0 || dst < 0 || src === dst ) return ;
const [ t ] = tabs . splice ( src , 1 ) ;
const insertAt = tabs . findIndex ( ( x ) => x . id === targetId ) ;
tabs . splice ( place === "after" ? insertAt + 1 : insertAt , 0 , t ) ;
emitTabs ( ) ;
} ) ;
2026-07-29 13:54:34 +02:00
ipcMain . handle ( "go-home" , ( ) => loadHome ( activeId ) ) ;
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
// --- Add-on framework -------------------------------------------------------
// Sidebar toggle + panel switching, driven from the chrome toolbar. `panelId`
// is the namespaced string the loader emits (`<addonId>:<panelId>`) — no
// coercion, main matches it verbatim.
ipcMain . handle ( "sidebar-toggle" , ( ) => { toggleSidebar ( ) ; return sidebarVisible ; } ) ;
2026-08-31 16:00:34 +02:00
// Drag events stream in from sidebar-preload while the user is holding the
// grip. Delta is px per mousemove; we clamp, layout, and debounce the save.
let _sidebarSaveTimer = null ;
ipcMain . handle ( "sidebar-drag" , ( _e , deltaPx ) => {
const d = Number ( deltaPx ) || 0 ;
const next = Math . max ( SIDEBAR _W _MIN , Math . min ( SIDEBAR _W _MAX , sidebarW + d ) ) ;
if ( next === sidebarW ) return sidebarW ;
sidebarW = next ;
layout ( ) ;
settings . sidebarWidth = sidebarW ;
clearTimeout ( _sidebarSaveTimer ) ;
_sidebarSaveTimer = setTimeout ( saveSettings , 400 ) ;
return sidebarW ;
} ) ;
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
ipcMain . handle ( "sidebar-open" , ( _e , panelId ) => { setSidebar ( true , panelId ) ; return sidebarVisible ; } ) ;
ipcMain . handle ( "sidebar-close" , ( ) => { setSidebar ( false ) ; return false ; } ) ;
ipcMain . handle ( "sidebar-state" , ( ) => ( {
visible : sidebarVisible ,
active : sidebarActivePanelId ,
panels : addonHost ? addonHost . getSidebarPanels ( ) : [ ] ,
} ) ) ;
// Read-side of Settings' Add-ons tab.
ipcMain . handle ( "addons-list" , ( ) => addonHost ? addonHost . snapshot ( ) : { installed : [ ] , sidebarPanels : [ ] } ) ;
// Toggle an add-on's enabled state. Discovery re-runs so newly-enabled
// add-ons activate immediately and newly-disabled ones drop out — no
// restart required.
ipcMain . handle ( "addons-set-enabled" , ( _e , id , enabled ) => {
if ( ! id || typeof id !== "string" ) return false ;
const disabled = new Set ( Array . isArray ( settings . disabledAddons ) ? settings . disabledAddons : [ ] ) ;
if ( enabled ) disabled . delete ( id ) ; else disabled . add ( id ) ;
settings . disabledAddons = [ ... disabled ] ;
saveSettings ( ) ;
// Rebuild the host so state matches settings.
if ( addonHost ) addonHost . discoverAndActivate ( ) ;
// Sidebar may need to close if its current panel came from an add-on we
// just disabled.
const panels = addonHost ? addonHost . getSidebarPanels ( ) : [ ] ;
if ( sidebarVisible && sidebarActivePanelId && ! panels . find ( ( p ) => p . panelId === sidebarActivePanelId ) ) {
sidebarActivePanelId = null ;
setSidebar ( false ) ;
}
return true ;
} ) ;
// Reveal an add-on's folder in the OS file manager — the primary way users
// edit / uninstall add-ons.
ipcMain . handle ( "addons-reveal" , ( _e , folder ) => {
if ( typeof folder !== "string" || ! folder ) return false ;
const norm = path . normalize ( folder ) ;
const base = addonsUserDir ( ) ;
if ( ! norm . toLowerCase ( ) . startsWith ( base . toLowerCase ( ) ) ) return false ; // don't leak arbitrary paths
try { shell . showItemInFolder ( norm ) ; return true ; } catch { return false ; }
} ) ;
ipcMain . handle ( "addons-open-dir" , ( ) => {
try { shell . openPath ( addonsUserDir ( ) ) ; return true ; } catch { return false ; }
} ) ;
ipcMain . handle ( "addons-reload" , ( ) => {
if ( ! addonHost ) return false ;
addonHost . discoverAndActivate ( ) ;
return true ;
} ) ;
// --- Add-on storage (origin-gated to <userData>/addons/<id>/...) ------------
// Add-on HTML pages get storage.get/set/all via sidebar-preload.js. Main
// derives the add-on id from the sender's file:// URL so a page can only
// touch its own store; any file:// outside addons/ returns nothing.
ipcMain . handle ( "addon-storage-get" , ( e , key , fallback ) => {
const id = addonIdForSender ( e . sender ) ;
if ( ! id ) return fallback ? ? null ;
try {
const raw = JSON . parse ( fs . readFileSync ( path . join ( addonsDataDir ( ) , id + ".json" ) , "utf8" ) ) ;
return key in raw ? raw [ key ] : ( fallback ? ? null ) ;
} catch { return fallback ? ? null ; }
} ) ;
ipcMain . handle ( "addon-storage-set" , ( e , key , value ) => {
const id = addonIdForSender ( e . sender ) ;
if ( ! id ) return false ;
if ( typeof key !== "string" || key . length > 128 ) return false ;
const file = path . join ( addonsDataDir ( ) , id + ".json" ) ;
let store = { } ;
try { store = JSON . parse ( fs . readFileSync ( file , "utf8" ) ) ; } catch { }
store [ key ] = value ;
try { fs . mkdirSync ( addonsDataDir ( ) , { recursive : true } ) ; fs . writeFileSync ( file , JSON . stringify ( store ) ) ; return true ; }
catch ( err ) { console . warn ( ` [addons] storage.set failed for ${ id } : ` , err ? . message ) ; return false ; }
} ) ;
ipcMain . handle ( "addon-storage-all" , ( e ) => {
const id = addonIdForSender ( e . sender ) ;
if ( ! id ) return { } ;
try { return JSON . parse ( fs . readFileSync ( path . join ( addonsDataDir ( ) , id + ".json" ) , "utf8" ) ) ; }
catch { return { } ; }
} ) ;
Theseus 0.1.3: branded error page for load failures (BUILT, NOT DEPLOYED)
Setup f2afc14efc63008cbb9dad44176e94146386db4c0afda4459f1d4eb929172b6d
Portable 5d08b1415526934db8de780949a610896064fe9567aa0e5e1702ebabd7eb7df2
Chromium's default 'This site can't be reached' replaced with a Theseus-
themed error page. did-fail-load on every tab's webContents (main frame
only, non-ignorable code) routes the tab to error.html with the
attempt URL, host, error code, and description as query params. The
page keeps t.url pointing at the failed URL so the address bar shows
what the user typed and they can edit + retry - refreshTabUrl's
existing file:// skip means the error page's own path never leaks
back into the bar.
Five kinds, chosen by pickErrorKind(code, host):
name-not-registered BCNR-eligible host + ERR_NAME_NOT_RESOLVED.
Says "no BCDN record on chain, no clearnet host
either." Offers Register on Sirius + Search +
Retry + Home.
name-unreachable ERR_NAME_NOT_RESOLVED on a non-BCNR host. DNS
failed - offers Retry + Search + Register +
Home.
unreachable CONN_REFUSED/RESET/TIMED_OUT/CLOSED/NETWORK_CHANGED.
Offers Retry + Tor guide + Home.
tls ERR_CERT_* range (-200..-299). Offers Retry +
Home.
generic Everything else.
home-preload.js gains `window.errorpage` alongside `window.home`. Both
APIs are sender-URL-gated in main - a random page seeing the shape
can't invoke them (isErrorPageSender / isHomePageSender). The external-
open handler additionally allowlists Silent Mode domains only.
package.json build.files gets error.html + error-preload.js so
electron-builder actually bundles them (GOTCHAS rule: an unlisted
runtime-loaded file silently opens blank).
Ship pages (releases-manifest.json, tools/index.html, releases/index.html,
site-theseus-x/index.html) updated to 0.1.3 with the new hashes.
DEPLOY STATUS - blocked on VPS SSH: my IP was hit with a full-port ban
mid-turn (likely fail2ban from the burst of scp during the 0.1.0-0.1.2
iterations). Site pages/manifest/installers are committed locally but
NOT yet on dl.silentmode.st or the Sia mirror. Live still reads 0.1.2.
User needs to unban 195.184.247.106 on their end, or wait for the ban
to expire, before the ship pages match reality.
2026-08-31 13:38:05 +02:00
// Error-page actions. All origin-gated to error.html so a third-party page
// that happens to see the API shape (home-preload exposes it on every tab)
// can't drive them.
ipcMain . handle ( "error-retry" , ( e , url ) => {
if ( ! isErrorPageSender ( e . sender ) ) return false ;
if ( typeof url !== "string" || ! url ) return false ;
navigateTab ( activeId , url ) ;
return true ;
} ) ;
ipcMain . handle ( "error-home" , ( e ) => {
if ( ! isErrorPageSender ( e . sender ) ) return false ;
loadHome ( activeId ) ;
return true ;
} ) ;
ipcMain . handle ( "error-search" , ( e , text ) => {
if ( ! isErrorPageSender ( e . sender ) ) return false ;
const q = String ( text || "" ) . trim ( ) ;
if ( ! q ) return false ;
navigateTab ( activeId , SEARCH ( q ) ) ;
return true ;
} ) ;
ipcMain . handle ( "error-register" , ( e , host ) => {
if ( ! isErrorPageSender ( e . sender ) ) return false ;
const h = String ( host || "" ) . trim ( ) . toLowerCase ( ) ;
if ( ! h ) return false ;
// Sirius's registrar UI takes ?prefill=<name>; if it ignores an unknown
// param the user just lands on the form and types it themselves.
const url = "https://sirius.x/register.html?prefill=" + encodeURIComponent ( h ) ;
navigateTab ( activeId , url ) ;
return true ;
} ) ;
ipcMain . handle ( "error-open-external" , ( e , url ) => {
if ( ! isErrorPageSender ( e . sender ) ) return false ;
// Allowlist Silent Mode domains only — no arbitrary external opens from
// a page that visits when things are already going wrong.
const ok = typeof url === "string" && /^https:\/\/(silentmode\.st|silentmode\.bch|sirius\.x|theseus\.x|navigate\.st)(\/|$)/i . test ( url ) ;
if ( ! ok ) return false ;
try { shell . openExternal ( url ) ; return true ; } catch { return false ; }
} ) ;
2026-08-31 15:35:47 +02:00
// Update chip: user clicked the download button → download the installer
// through Theseus itself. session.downloadURL triggers the same
// will-download handler our own downloads panel listens on, so the file
// lands in the user's Downloads folder AND appears in the in-app
// downloads chip with progress + Show-in-folder. No system browser
// jump, no "why did another browser open?" confusion.
Theseus 0.0.4: cheap in-app update-check chip
Bumps version so the chip actually surfaces itself on 0.0.3 installs
(the version-newer check requires a strict semver bump — same-version
rebuilds don't trigger the chip). From this release on, whenever the
manifest names a newer Theseus, users get a one-click download.
Mechanism
- main.js checkForUpdate() fetches https://dl.silentmode.st/releases-
manifest.json on startup (5s timeout, cache: no-store) + every 6h.
Finds the theseus-navigator release, compares version to
app.getVersion() with a numeric a.b.c comparator that handles
"0.10.0 > 0.9.9" correctly.
- On a match → stores { version, setupUrl, portableUrl, setupHash,
portableHash, date } and emits update-available to chrome. Cleared
after the user upgrades + relaunches (same-version → null).
- Re-emits on chrome's did-finish-load in case the fetch beats the
chrome view.
Chip UI (chrome.html)
- Acid-yellow pill between the downloads button and the Tor toggle:
"↓ Update to X.Y.Z" + a ✕. Main body opens setupUrl in the system
browser via shell.openExternal (origin-validated to
https://dl.silentmode.st/ or https://silentmode.st/). ✕ dismisses
for the current session — you'll see it again next launch if still
behind.
Trust anchor
- No signing / no cryptographic verification of the download in this
phase. releases.silentmode.bch publishes the SAME manifest URL, so
users who want to verify can cross-check the manifest hash against
what BCNR returns. The proper auto-updater with signature checks is
the follow-on to this cheap version.
Non-goals in phase 1
- No delta downloads; the user clicks and gets a full installer.
- No auto-install; download → user runs the installer themselves.
- No "check now" button in Settings; the periodic timer suffices.
- No portable-vs-installed detection; the chip prefers setupUrl (the
installer upgrades in place). Right-click for portable is future work.
2026-08-28 20:21:44 +02:00
ipcMain . handle ( "open-update-download" , ( _e , url ) => {
const ok = typeof url === "string" && ( url . startsWith ( "https://dl.silentmode.st/" ) || url . startsWith ( "https://silentmode.st/" ) ) ;
if ( ! ok ) return false ;
2026-08-31 15:35:47 +02:00
try {
// The downloads toolbar button spins + shows a progress badge as
// will-download / did-update updates fire, so the user sees the
// transfer without us having to force-open the downloads panel.
session . defaultSession . downloadURL ( url ) ;
return true ;
} catch ( e ) { console . warn ( "update download failed:" , e ? . message ) ; return false ; }
Theseus 0.0.4: cheap in-app update-check chip
Bumps version so the chip actually surfaces itself on 0.0.3 installs
(the version-newer check requires a strict semver bump — same-version
rebuilds don't trigger the chip). From this release on, whenever the
manifest names a newer Theseus, users get a one-click download.
Mechanism
- main.js checkForUpdate() fetches https://dl.silentmode.st/releases-
manifest.json on startup (5s timeout, cache: no-store) + every 6h.
Finds the theseus-navigator release, compares version to
app.getVersion() with a numeric a.b.c comparator that handles
"0.10.0 > 0.9.9" correctly.
- On a match → stores { version, setupUrl, portableUrl, setupHash,
portableHash, date } and emits update-available to chrome. Cleared
after the user upgrades + relaunches (same-version → null).
- Re-emits on chrome's did-finish-load in case the fetch beats the
chrome view.
Chip UI (chrome.html)
- Acid-yellow pill between the downloads button and the Tor toggle:
"↓ Update to X.Y.Z" + a ✕. Main body opens setupUrl in the system
browser via shell.openExternal (origin-validated to
https://dl.silentmode.st/ or https://silentmode.st/). ✕ dismisses
for the current session — you'll see it again next launch if still
behind.
Trust anchor
- No signing / no cryptographic verification of the download in this
phase. releases.silentmode.bch publishes the SAME manifest URL, so
users who want to verify can cross-check the manifest hash against
what BCNR returns. The proper auto-updater with signature checks is
the follow-on to this cheap version.
Non-goals in phase 1
- No delta downloads; the user clicks and gets a full installer.
- No auto-install; download → user runs the installer themselves.
- No "check now" button in Settings; the periodic timer suffices.
- No portable-vs-installed detection; the chip prefers setupUrl (the
installer upgrades in place). Right-click for portable is future work.
2026-08-28 20:21:44 +02:00
} ) ;
ipcMain . handle ( "dismiss-update" , ( ) => { updateDismissedThisSession = true ; emitUpdateAvailable ( ) ; return true ; } ) ;
ipcMain . handle ( "recheck-update" , async ( ) => { await checkForUpdate ( ) ; return updateAvailable ; } ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
// Home page editable cards. Origin-gated to home.html — random pages that
// snoop the preload can't act on the local file.
ipcMain . handle ( "home-cards-get" , ( e ) => isHomePageSender ( e . sender ) ? loadHomeCards ( ) : [ ] ) ;
ipcMain . handle ( "home-cards-set" , ( e , cards ) => { if ( ! isHomePageSender ( e . sender ) ) return false ; if ( ! Array . isArray ( cards ) ) return false ; saveHomeCards ( cards ) ; return true ; } ) ;
ipcMain . handle ( "home-cards-reset" , ( e ) => { if ( ! isHomePageSender ( e . sender ) ) return false ; try { fs . unlinkSync ( homeCardsFile ( ) ) ; } catch { } return true ; } ) ;
2026-07-29 13:54:34 +02:00
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 ( ) ; } ) ;
2026-08-31 02:50:57 +02:00
ipcMain . handle ( "reload" , ( _e , hard ) => {
const wc = activeTab ( ) ? . view . webContents ;
if ( ! wc ) return ;
try { hard ? wc . reloadIgnoringCache ( ) : wc . reload ( ) ; } catch { }
} ) ;
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
// Optional persistent memory ("Always use…" from the popover switcher).
2026-08-02 11:39:00 +02:00
rememberCollision ( host , tld , choice , arg ? . remember || "no" ) ;
2026-08-02 18:40:38 +02:00
const registry = registryOf ( tld ) ;
// DIRECT navigation — bypass navigateTab/loadBns entirely so nothing in the
// collision-decision path can trigger a re-prompt on an explicit user switch.
// The user clicked the switcher; they've made their choice. Just load it.
setLoading ( t , true ) ;
t . internalNav = true ;
try {
if ( choice === "icann" ) {
t . url = "https://" + host + "/" ;
await t . view . webContents . loadURL ( t . url ) ;
t . prov = { host , kind : "web" , note : "switched to ICANN" , tld , registry } ;
} else {
2026-08-02 19:26:47 +02:00
t . url = "https://" + host + "/" ; // display: https://; internal fetch: bns://
2026-08-02 18:40:38 +02:00
await t . view . webContents . loadURL ( ` bns:// ${ host } / ` ) ;
const rec = entries . get ( host ) ;
const r = rec ? . entry ? . records || { } ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
// Mirror serveBns's subdomain-first-ip rule so the badge does not lie.
const _isSub = rec ? . entry ? . name && host !== rec . entry . name ;
const src = ( _isSub && r . ip ) ? "direct server"
: r . h ? "on-chain (chain)"
: r . s3 ? "Sia network"
: r . ip ? "direct server"
: r . u ? "redirect"
: "record" ;
2026-08-02 18:40:38 +02:00
t . prov = { host , kind : "ok" , source : src , category : rec ? . entry ? . category , records : Object . keys ( r ) , tld , registry } ;
}
} catch ( e ) { console . warn ( "collision-switch load failed:" , e ? . message ) ; }
finally { t . internalNav = false ; setLoading ( t , false ) ; }
if ( t . id === activeId ) pushNav ( t . prov ) ;
emitTabs ( ) ;
2026-08-02 11:39:00 +02:00
} ) ;
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 ; } ) ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
// Storage: clear right now (any subset). "history" also drops the saved-session file.
// ---- Password vault -------------------------------------------------------
// The vault lives at userData/passwords.vault (encrypted). Unlock state is
// held in this main-process closure only — never sent to a renderer except
// in the explicit response to password-get(id). Cleared on quit alongside
// the other storage clears (see before-quit hook).
const vaultFile = ( ) => path . join ( app . getPath ( "userData" ) , "passwords.vault" ) ;
let vaultState = null ; // { key, purposeRoot, entries, _salt, _iters }
const vaultOk = ( ) => ( { ok : true } ) ;
const vaultErr = ( m ) => ( { ok : false , err : String ( m ) } ) ;
ipcMain . handle ( "password-status" , ( ) => ( {
setup : fs . existsSync ( vaultFile ( ) ) ,
unlocked : ! ! vaultState ,
} ) ) ;
ipcMain . handle ( "password-setup" , async ( _e , { masterPassword , seedSource } ) => {
try {
if ( ! masterPassword || String ( masterPassword ) . length < 4 ) return vaultErr ( "master password too short" ) ;
if ( fs . existsSync ( vaultFile ( ) ) ) return vaultErr ( "vault already exists" ) ;
const v = await loadVaultLib ( ) ;
2026-08-19 00:38:52 +02:00
let purposeRootHex , messengerRootHex ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
if ( seedSource && seedSource . kind === "mnemonic" && seedSource . mnemonic ) {
2026-08-19 00:38:52 +02:00
// Same seed, two purpose roots — one for password derivation, one for
// the Nostr messaging identity. Storing both means Hermes can bind to
// the vault so the user never re-enters the mnemonic. Different HKDF
// info strings keep the two subtrees cryptographically disjoint.
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
const seed = await v . bip39ToSeed ( String ( seedSource . mnemonic ) ) ;
2026-08-19 00:38:52 +02:00
purposeRootHex = v . bytesToHex ( await v . seedToPurposeRoot ( seed , "passwords/0" ) ) ;
messengerRootHex = v . bytesToHex ( await v . seedToPurposeRoot ( seed , "messenger/0" ) ) ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
} else {
2026-08-19 00:38:52 +02:00
// Independent random seed — 32 bytes of purposeRoot directly. No mnemonic
// means no messenger root; Hermes will fall back to its own mnemonic entry.
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
const root = require ( "node:crypto" ) . webcrypto . getRandomValues ( new Uint8Array ( 32 ) ) ;
purposeRootHex = v . bytesToHex ( root ) ;
}
2026-08-19 00:38:52 +02:00
vaultState = await v . createVault ( vaultFile ( ) , masterPassword , purposeRootHex ,
messengerRootHex ? { messengerRootHex } : { } ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
emitPwAvailability ( ) ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
return vaultOk ( ) ;
} catch ( e ) { return vaultErr ( e ? . message || e ) ; }
} ) ;
ipcMain . handle ( "password-unlock" , async ( _e , masterPassword ) => {
try {
if ( ! fs . existsSync ( vaultFile ( ) ) ) return vaultErr ( "no vault" ) ;
const v = await loadVaultLib ( ) ;
vaultState = await v . unlockVault ( vaultFile ( ) , masterPassword ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
emitPwAvailability ( ) ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
return { ok : true , entries : v . listMetadata ( vaultState ) } ;
} catch ( e ) { return vaultErr ( e ? . message || e ) ; }
} ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
ipcMain . handle ( "password-lock" , ( ) => { vaultState = null ; emitPwAvailability ( ) ; return true ; } ) ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
ipcMain . handle ( "password-list" , async ( ) => {
if ( ! vaultState ) return { ok : false , err : "locked" } ;
const v = await loadVaultLib ( ) ;
return { ok : true , entries : v . listMetadata ( vaultState ) } ;
} ) ;
ipcMain . handle ( "password-get" , async ( _e , id ) => {
if ( ! vaultState ) return vaultErr ( "locked" ) ;
try {
const v = await loadVaultLib ( ) ;
const password = await v . resolvePassword ( vaultState , id ) ;
return { ok : true , password } ;
} catch ( e ) { return vaultErr ( e ? . message || e ) ; }
} ) ;
ipcMain . handle ( "password-add" , async ( _e , spec ) => {
if ( ! vaultState ) return vaultErr ( "locked" ) ;
try {
const v = await loadVaultLib ( ) ;
const entry = v . newEntry ( spec || { } ) ;
vaultState . entries . push ( entry ) ;
await v . saveVault ( vaultFile ( ) , vaultState ) ;
return { ok : true , id : entry . id , entries : v . listMetadata ( vaultState ) } ;
} catch ( e ) { return vaultErr ( e ? . message || e ) ; }
} ) ;
ipcMain . handle ( "password-update" , async ( _e , id , patch ) => {
if ( ! vaultState ) return vaultErr ( "locked" ) ;
try {
const v = await loadVaultLib ( ) ;
const e = vaultState . entries . find ( ( x ) => x . id === id ) ;
if ( ! e ) return vaultErr ( "no such entry" ) ;
// Whitelist mutable fields; never let the renderer overwrite id/addedAt.
for ( const k of [ "domain" , "username" , "literal" , "generated" ] ) if ( patch && k in patch ) e [ k ] = patch [ k ] ;
// Switching between literal and generated: drop the other field.
if ( patch && "literal" in patch ) delete e . generated ;
if ( patch && "generated" in patch ) delete e . literal ;
await v . saveVault ( vaultFile ( ) , vaultState ) ;
return { ok : true , entries : v . listMetadata ( vaultState ) } ;
} catch ( e ) { return vaultErr ( e ? . message || e ) ; }
} ) ;
ipcMain . handle ( "password-remove" , async ( _e , id ) => {
if ( ! vaultState ) return vaultErr ( "locked" ) ;
try {
const v = await loadVaultLib ( ) ;
vaultState . entries = vaultState . entries . filter ( ( x ) => x . id !== id ) ;
await v . saveVault ( vaultFile ( ) , vaultState ) ;
return { ok : true , entries : v . listMetadata ( vaultState ) } ;
} catch ( e ) { return vaultErr ( e ? . message || e ) ; }
} ) ;
ipcMain . handle ( "password-generate" , async ( _e , { domain , username = "" , version = 1 , rules } = { } ) => {
if ( ! vaultState ) return vaultErr ( "locked" ) ;
try {
const v = await loadVaultLib ( ) ;
const password = await v . derivePassword ( vaultState . purposeRoot , { domain , username , version , rules } ) ;
return { ok : true , password } ;
} catch ( e ) { return vaultErr ( e ? . message || e ) ; }
} ) ;
ipcMain . handle ( "clear-browsing-data" , async ( _e , opts ) => {
const o = opts || { } ;
await clearBrowsingData ( { cookies : ! ! o . cookies , cache : ! ! o . cache , storage : ! ! o . storage } ) ;
if ( o . history ) await clearHistoryNow ( ) ;
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 ) => {
2026-08-06 01:41:31 +02:00
// A custom engine needs a name and a URL template containing "%s". Adding
// installs it in both the settings list AND the toolbar dropdown.
2026-07-30 20:58:45 +02:00
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-08-06 01:41:31 +02:00
// Custom engines are auto-installed and enabled.
settings . enabledEngines = [ ... new Set ( [ ... ( settings . enabledEngines || DEFAULT _ENABLED ) , id ] ) ] ;
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 ) => {
2026-08-06 01:41:31 +02:00
// Drop a custom engine entirely — from customEngines and any list that
// referenced it.
2026-07-30 20:58:45 +02:00
settings . customEngines = ( settings . customEngines || [ ] ) . filter ( ( e ) => e . id !== id ) ;
2026-08-06 01:41:31 +02:00
settings . enabledEngines = ( settings . enabledEngines || DEFAULT _ENABLED ) . filter ( ( x ) => x !== id ) ;
if ( settings . searchEngine === id ) settings . searchEngine = enabledEnginesList ( ) [ 0 ] ? . id || "duckduckgo" ;
saveSettings ( ) ; emitEngines ( ) ;
return { engines : allEngines ( ) , current : settings . searchEngine } ;
} ) ;
// Right-click "Remove from list": drops a built-in from installedEngines AND
// enabledEngines so it goes back to the catalog. For custom engines this
// aliases to remove-engine (they don't live in installedEngines).
ipcMain . handle ( "remove-from-list" , ( _e , id ) => {
if ( ( settings . customEngines || [ ] ) . some ( ( e ) => e . id === id ) ) {
settings . customEngines = ( settings . customEngines || [ ] ) . filter ( ( e ) => e . id !== id ) ;
} else if ( SEARCH _ENGINES [ id ] ) {
settings . installedEngines = ( settings . installedEngines || DEFAULT _ENABLED ) . filter ( ( x ) => x !== id ) ;
} else {
return { engines : allEngines ( ) , current : settings . searchEngine } ;
}
settings . enabledEngines = ( settings . enabledEngines || DEFAULT _ENABLED ) . filter ( ( x ) => x !== id ) ;
if ( settings . enabledEngines . length === 0 ) settings . enabledEngines = [ "duckduckgo" ] ; // never empty
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-08-06 01:41:31 +02:00
// Enable/disable a built-in engine. Enabling from the catalog also INSTALLS it
// (adds to installedEngines). Disabling only removes it from enabledEngines —
// it stays in installedEngines so the row remains visible with the toggle off.
2026-07-30 22:55:52 +02:00
ipcMain . handle ( "set-engine-enabled" , ( _e , id , on ) => {
if ( SEARCH _ENGINES [ id ] ) {
2026-08-06 01:41:31 +02:00
let installed = ( settings . installedEngines || DEFAULT _ENABLED ) . slice ( ) ;
let enabled = ( settings . enabledEngines || DEFAULT _ENABLED ) . filter ( ( x ) => x !== id ) ;
if ( on ) {
if ( ! installed . includes ( id ) ) installed . push ( id ) ;
enabled . push ( id ) ;
}
settings . installedEngines = installed ;
settings . enabledEngines = enabled . length ? enabled : [ "duckduckgo" ] ; // never empty
2026-07-30 22:55:52 +02:00
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 ) ;
} ) ;
2026-08-06 01:41:31 +02:00
ipcMain . handle ( "picker-open-settings" , ( ) => {
showEnginePicker ( false ) ;
const focus = ( t ) => { try { t . view . webContents . send ( "focus-section" , "search" ) ; } catch { } } ;
const ex = tabs . find ( ( t ) => t . settings ) ;
if ( ex ) { setActive ( ex . id ) ; focus ( ex ) ; return ; }
const id = createTab ( null , { settings : true } ) ;
const t = tabById ( id ) ;
if ( t ) t . view . webContents . once ( "did-finish-load" , ( ) => focus ( t ) ) ;
} ) ;
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 ( ) ; } ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
// Address-bar suggestions: show/hide, forward arrow-keys to the dropdown.
ipcMain . handle ( "suggest-address" , ( _e , query , rect ) => {
const suggestions = historySearch ( query ) ;
if ( ! suggestions . length ) { showAddressPicker ( false ) ; return ; }
if ( rect ) { apPos = { x : Math . round ( rect . x ) , y : Math . round ( rect . y ) } ; apW = Math . max ( 280 , Math . round ( rect . w || 520 ) ) ; }
showAddressPicker ( true , suggestions ) ;
} ) ;
ipcMain . handle ( "close-address-picker" , ( ) => showAddressPicker ( false ) ) ;
ipcMain . handle ( "address-picker-resize" , ( _e , h ) => {
apH = Math . max ( 40 , Math . min ( 400 , Math . round ( h ) || 60 ) ) ;
if ( apVisible ) positionAddressPicker ( ) ;
} ) ;
Ship Theseus 0.0.8: window.bcnr dApp API + eTLD+1 permission origins
Merges a parallel session's work with the multi-source BNS story from 0.0.7.
The dApp side (parallel session)
--------------------------------
* bcnr-preload.js — installs `window.bcnr` on every page via contextBridge.
Read-only surface: resolveName(name), isRegistered(name), getBcnrTlds(),
getRecordVersion(name), plus getPermissionOrigin() for diagnostics. All
Promises; a missing name returns null (not throw). No signing, no wallet
unlock — that surface is designed but deliberately out of scope for 0.0.8
(see TheseusNavigator/DESIGN-integrated-wallet.md).
* bcnr-origin.js — pure function that computes the eTLD+1 permission origin
for a URL. ICANN suffixes via `psl` (same PSL Chromium uses, handles
.co.uk / .github.io / etc); BNS names key off the on-chain TLD list so
foo.wallet becomes a public suffix as soon as `wallet` appears there.
Match browser cookie / MetaMask semantics: a grant on pay.merchant.com
covers account.merchant.com but not evil.com.
* dev/bcnr-selftest.js, dev/origin-selftest.mjs — self-tests, no I/O.
* main.js wires bcnr-preload.js into session.defaultSession.setPreloads() so
it runs BEFORE per-WebContentsView preloads; adds bcnr:* IPC handlers.
* preload.js + chrome.html — small hooks so the shell picks up window.bcnr
the same way regular content does.
* package.json — psl dep, bcnr-preload.js/bcnr-origin.js in `files`.
Also included
-------------
* AriadneResolver/mobile/.../UpdateCheck.java — in-app update-check for the
Android app; already active in the shipped 0.11 APK (build.ps1 -Recurse
picked it up), formalising the source now.
* TheseusNavigator/snapshots/bns-name-snapshot.json — refreshed bundled
starter (73 beacon txs, root c37b8596…c54e414ba).
* Site pages + manifest updated to point at 0.0.8.
TheseusNavigator-Setup-0.0.8.exe 95.4 MB
21939743eafdfe8742a6b7c4b987bd2782384d7bc41289cb80a7e08019dc9f02
TheseusNavigator-0.0.8-portable.exe 92.7 MB
2aa429fe39dc0fa4ac040fc6d6eb31b0f890c8a83175c49fcb50f052c480d39d
2026-08-31 01:38:55 +02:00
ipcMain . handle ( "address-pick" , ( _e , url ) => {
showAddressPicker ( false ) ;
if ( ! url ) return ;
const u = String ( url ) ;
// Push the picked URL to the chrome renderer directly so the address bar
// shows the full URL immediately. The tabs event's focus guard
// (document.activeElement !== $("url"))
// skips its value overwrite while the URL input still has DOM focus, and
// clicking a WebContentsView sibling doesn't always deliver the blur to
// the chrome renderer in time — user was left staring at their 3-letter
// typed query while the picked URL loaded behind it.
try { chrome ? . webContents . send ( "address-picked" , u ) ; } catch { }
navigateTab ( activeId , u ) ;
} ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
// Password fill — chip in the toolbar opens a picker of matching credentials
// for the current site. Clicking a match injects the fill script into the
// active tab. Whole flow is user-initiated; no page-load DOM watchers yet.
ipcMain . handle ( "toggle-pw-fill" , async ( _e , rect ) => {
if ( pwfVisible ) return showPwFill ( false ) ;
const t = activeTab ( ) ; const host = t ? . prov ? . host || "" ;
const matches = pwMatchesForHost ( host ) ;
if ( ! matches . length ) return showPwFill ( false ) ;
if ( rect ) pwfPos = { x : Math . round ( rect . x ) , y : Math . round ( rect . y ) } ;
showPwFill ( true , matches ) ;
} ) ;
ipcMain . handle ( "close-pw-fill" , ( ) => showPwFill ( false ) ) ;
2026-08-29 15:49:30 +02:00
ipcMain . handle ( "link-status-resize" , ( _e , w , h ) => {
linkStatusW = Math . max ( 60 , Math . min ( 2000 , Math . round ( w ) || 100 ) ) ;
linkStatusH = Math . max ( 20 , Math . min ( 60 , Math . round ( h ) || 22 ) ) ;
if ( linkStatusVisible ) positionLinkStatus ( ) ;
} ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
ipcMain . handle ( "pw-fill-resize" , ( _e , h ) => {
pwfH = Math . max ( 60 , Math . min ( 300 , Math . round ( h ) || 80 ) ) ;
if ( pwfVisible ) positionPwFill ( ) ;
} ) ;
ipcMain . handle ( "pw-fill-pick" , async ( _e , id ) => {
showPwFill ( false ) ;
if ( ! vaultState ) return { ok : false , err : "locked" } ;
try {
const v = await loadVaultLib ( ) ;
const entry = vaultState . entries . find ( ( x ) => x . id === id ) ;
if ( ! entry ) return { ok : false , err : "no such entry" } ;
const password = await v . resolvePassword ( vaultState , id ) ;
return await pwFillIntoActiveTab ( { username : entry . username , password } ) ;
} catch ( e ) { return { ok : false , err : e ? . message || String ( e ) } ; }
} ) ;
// The chrome sends arrow-up/down/enter through so the picker can move its
// selection cursor without stealing focus from the address input.
ipcMain . handle ( "address-cursor" , ( _e , dir ) => {
if ( apVisible && addressPicker ) try { addressPicker . webContents . send ( "address-cursor" , dir ) ; } catch { }
} ) ;
2026-08-02 11:39:00 +02:00
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 ) ;
} ) ;
Hermes: wire NIP-17 messaging into Theseus, provision chipnet hermes.bch
- Hermes/proof/: standalone keystone — BIP-39→Nostr, NIP-17 wrap/unwrap,
name-addressed send/receive verified end-to-end via public relay
- Argus/src/lib/hermes-derive.js: HKDF(silentmode/messenger/0) using libauth
so the registrar can compute np without pulling nostr-tools into Argus
- Argus/src/register-hermes-chipnet.mjs: idempotent REG/UPD script; publishes
np (Nostr pubkey) and nr (relay) records for a chipnet name
- TheseusNavigator/lib/hermes.js: same derivation + wrap/unwrap + record
parsing, canonical for Theseus; guarded by lib/package.json type:module
- TheseusNavigator/messages.html + messages-preload.js: Messages panel UI
(identity from mnemonic, live inbox, compose by .bch name), .bch suffix
stripped from displayed names
- TheseusNavigator/main.js: HERMES_MOD + loadHermesLib next to VAULT_MOD;
ipc handlers (hermes-status/init/close/inbox/send/open), per-relay
subscription with auto-reconnect, status pushed on WS open/close,
reverse-resolve pk -> .bch name via cached BNS index, Ctrl+Shift+M shortcut
via web-contents-created (works from any tab)
- Chipnet hermes.bch registered with np=f2c92519...67f4 nr=wss://nos.lol
(txid 7fc6de4544c13b782f163fdb892ea6749785886d05a336282fa659d722c7e92b);
end-to-end verified in Theseus
2026-08-18 20:14:43 +02:00
// ---- Hermes messages panel -------------------------------------------------
// A separate BrowserWindow (opens on Ctrl+Shift+M anywhere in Theseus). Uses
// the wallet mnemonic to derive the Nostr identity in-memory only — the seed
// and secret key are never written to disk. The panel process holds one
// WebSocket per relay in HERMES_DEFAULT_RELAYS for the receive subscription,
// and opens a per-send WebSocket for publishes.
const HERMES _DEFAULT _RELAYS = [ "wss://nos.lol" ] ;
const HERMES _INBOX _LIMIT = 200 ;
let hermesWin = null ;
// State when initialised: { skHex, pkHex, npub, inbox: [], relays: Map<url, { ws, ready }> }
// skHex is held instead of the raw Uint8Array so it can be Buffer-restored per operation
// (nostr-tools expects Uint8Array; converting on demand keeps the surface easier to reason about).
let hermesState = null ;
const hOk = ( o = { } ) => ( { ok : true , ... o } ) ;
const hErr = ( e ) => ( { ok : false , err : String ( ( e && e . message ) || e ) } ) ;
function hermesEmit ( channel , payload ) {
if ( hermesWin && ! hermesWin . isDestroyed ( ) ) hermesWin . webContents . send ( channel , payload ) ;
}
function hermesRecord ( msg ) {
hermesState . inbox . push ( msg ) ;
if ( hermesState . inbox . length > HERMES _INBOX _LIMIT ) {
hermesState . inbox . splice ( 0 , hermesState . inbox . length - HERMES _INBOX _LIMIT ) ;
}
hermesEmit ( "hermes-message" , msg ) ;
}
// Pushed on every relay connect/disconnect so the pill in the panel reflects
// reality without polling. Cheap; sent to the renderer whenever a socket
// transitions ready/not-ready.
function hermesEmitStatus ( ) {
if ( ! hermesState ) return ;
let connected = 0 ;
for ( const s of hermesState . relays . values ( ) ) if ( s . ready ) connected ++ ;
hermesEmit ( "hermes-status-update" , {
relaysConnected : connected ,
relaysTotal : hermesState . relays . size ,
} ) ;
}
// Reverse-resolve a pkHex → .bch name by scanning the (cached) BNS index.
// Populates senderName in the inbox so incoming DMs render as
// "alice.bch · 12ab…9f" instead of a naked hex string. Cache is per-hermesState
// (dropped on hermes-close) so a re-init starts clean.
async function hermesReverseResolve ( pkHex ) {
if ( ! hermesState ) return null ;
if ( ! hermesState . _pkToName ) hermesState . _pkToName = new Map ( ) ;
if ( hermesState . _pkToName . has ( pkHex ) ) return hermesState . _pkToName . get ( pkHex ) ;
try {
const R = await getResolver ( ) ;
const H = await loadHermesLib ( ) ;
const idx = await R . buildIndex ( { WebSocket } ) ;
for ( const [ name , entry ] of idx ) {
const raw = entry && entry . records && entry . records . np ;
if ( typeof raw !== "string" || ! raw . trim ( ) ) continue ;
try {
const hex = H . parseNpRecord ( raw ) ;
if ( hex === pkHex ) {
hermesState . _pkToName . set ( pkHex , name ) ;
return name ;
}
} catch { /* malformed np — skip */ }
}
} catch { /* chain unreachable — leave unresolved this round */ }
hermesState . _pkToName . set ( pkHex , null ) ; // negative-cache so we don't re-scan every message
return null ;
}
// One receive subscription per relay. If the socket dies we resurrect it on a
// backoff — a locked / logged-out state tears them all down cleanly.
async function hermesConnectRelay ( url ) {
const H = await loadHermesLib ( ) ;
const state = { ws : null , ready : false , subId : "hermes-inbox" } ;
const open = ( ) => {
if ( ! hermesState ) return ; // torn down while reconnecting
const ws = new WebSocket ( url ) ;
state . ws = ws ;
ws . on ( "open" , ( ) => {
state . ready = true ;
hermesEmitStatus ( ) ;
ws . send ( JSON . stringify ( [ "REQ" , state . subId , { kinds : [ 1059 ] , "#p" : [ hermesState . pkHex ] } ] ) ) ;
} ) ;
ws . on ( "message" , async ( buf ) => {
let msg ; try { msg = JSON . parse ( buf . toString ( ) ) ; } catch { return ; }
if ( msg [ 0 ] !== "EVENT" || msg [ 1 ] !== state . subId ) return ;
try {
const sk = Buffer . from ( hermesState . skHex , "hex" ) ;
const opened = H . unwrapChat ( { receiverSk : sk , wrap : msg [ 2 ] } ) ;
// Dedupe: a wrap arriving from multiple relays produces the same rumor id
// (kind:14 hash), but since we don't expose the rumor id here we dedupe
// on (senderPkHex, text, createdAt) which is sufficient for MVP.
const key = opened . senderPkHex + "\0" + opened . createdAt + "\0" + opened . text ;
if ( hermesState . _seen && hermesState . _seen . has ( key ) ) return ;
hermesState . _seen && hermesState . _seen . add ( key ) ;
// Reverse-resolve pk → name off the wrap decrypt path (fire-and-forget
// wouldn't work — we need the name in the record we push). Await here;
// the cache short-circuits after the first miss per pk.
const senderName = await hermesReverseResolve ( opened . senderPkHex ) ;
hermesRecord ( {
senderPkHex : opened . senderPkHex ,
senderName ,
text : opened . text ,
createdAt : opened . createdAt ,
} ) ;
} catch { /* unwrap failure = not for us, or malformed — drop silently */ }
} ) ;
ws . on ( "close" , ( ) => {
state . ready = false ;
hermesEmitStatus ( ) ;
// Attempt reconnect if we're still supposed to be running.
if ( hermesState && hermesState . relays . get ( url ) === state ) {
setTimeout ( open , 3000 ) ;
}
} ) ;
ws . on ( "error" , ( ) => { /* close handler will retry */ } ) ;
} ;
open ( ) ;
return state ;
}
function hermesTeardown ( ) {
if ( ! hermesState ) return ;
for ( const state of hermesState . relays . values ( ) ) {
try { state . ws ? . close ( 1000 ) ; } catch { }
}
hermesState = null ;
}
ipcMain . handle ( "hermes-status" , ( ) => {
if ( ! hermesState ) return hOk ( { ready : false } ) ;
let connected = 0 ;
for ( const s of hermesState . relays . values ( ) ) if ( s . ready ) connected ++ ;
return hOk ( {
ready : true ,
npub : hermesState . npub ,
pkHex : hermesState . pkHex ,
relaysConnected : connected ,
relaysTotal : hermesState . relays . size ,
} ) ;
} ) ;
2026-08-19 00:38:52 +02:00
// Init modes:
// { mnemonic: "..." } — derive from BIP-39 mnemonic (typed by user)
// { useVault: true } — reuse the password vault's messenger root; no re-entry
// required. Available iff vault is unlocked AND was set
// up from a mnemonic (so messengerRoot was persisted).
ipcMain . handle ( "hermes-init" , async ( _e , opts = { } ) => {
Hermes: wire NIP-17 messaging into Theseus, provision chipnet hermes.bch
- Hermes/proof/: standalone keystone — BIP-39→Nostr, NIP-17 wrap/unwrap,
name-addressed send/receive verified end-to-end via public relay
- Argus/src/lib/hermes-derive.js: HKDF(silentmode/messenger/0) using libauth
so the registrar can compute np without pulling nostr-tools into Argus
- Argus/src/register-hermes-chipnet.mjs: idempotent REG/UPD script; publishes
np (Nostr pubkey) and nr (relay) records for a chipnet name
- TheseusNavigator/lib/hermes.js: same derivation + wrap/unwrap + record
parsing, canonical for Theseus; guarded by lib/package.json type:module
- TheseusNavigator/messages.html + messages-preload.js: Messages panel UI
(identity from mnemonic, live inbox, compose by .bch name), .bch suffix
stripped from displayed names
- TheseusNavigator/main.js: HERMES_MOD + loadHermesLib next to VAULT_MOD;
ipc handlers (hermes-status/init/close/inbox/send/open), per-relay
subscription with auto-reconnect, status pushed on WS open/close,
reverse-resolve pk -> .bch name via cached BNS index, Ctrl+Shift+M shortcut
via web-contents-created (works from any tab)
- Chipnet hermes.bch registered with np=f2c92519...67f4 nr=wss://nos.lol
(txid 7fc6de4544c13b782f163fdb892ea6749785886d05a336282fa659d722c7e92b);
end-to-end verified in Theseus
2026-08-18 20:14:43 +02:00
try {
hermesTeardown ( ) ;
const H = await loadHermesLib ( ) ;
2026-08-19 00:38:52 +02:00
let sk , pkHex , npub ;
if ( opts . useVault ) {
if ( ! vaultState || ! vaultState . messengerRoot ) {
return hErr ( "password vault is locked or was set up without a mnemonic" ) ;
}
( { sk , pkHex , npub } = H . nostrKeyFromRoot ( vaultState . messengerRoot ) ) ;
} else {
const mnemonic = opts . mnemonic ;
if ( ! mnemonic || typeof mnemonic !== "string" || mnemonic . trim ( ) . split ( /\s+/ ) . length < 12 ) {
return hErr ( "enter a 12- or 24-word mnemonic" ) ;
}
( { sk , pkHex , npub } = await H . nostrKeyFromMnemonic ( mnemonic . trim ( ) ) ) ;
}
Hermes: wire NIP-17 messaging into Theseus, provision chipnet hermes.bch
- Hermes/proof/: standalone keystone — BIP-39→Nostr, NIP-17 wrap/unwrap,
name-addressed send/receive verified end-to-end via public relay
- Argus/src/lib/hermes-derive.js: HKDF(silentmode/messenger/0) using libauth
so the registrar can compute np without pulling nostr-tools into Argus
- Argus/src/register-hermes-chipnet.mjs: idempotent REG/UPD script; publishes
np (Nostr pubkey) and nr (relay) records for a chipnet name
- TheseusNavigator/lib/hermes.js: same derivation + wrap/unwrap + record
parsing, canonical for Theseus; guarded by lib/package.json type:module
- TheseusNavigator/messages.html + messages-preload.js: Messages panel UI
(identity from mnemonic, live inbox, compose by .bch name), .bch suffix
stripped from displayed names
- TheseusNavigator/main.js: HERMES_MOD + loadHermesLib next to VAULT_MOD;
ipc handlers (hermes-status/init/close/inbox/send/open), per-relay
subscription with auto-reconnect, status pushed on WS open/close,
reverse-resolve pk -> .bch name via cached BNS index, Ctrl+Shift+M shortcut
via web-contents-created (works from any tab)
- Chipnet hermes.bch registered with np=f2c92519...67f4 nr=wss://nos.lol
(txid 7fc6de4544c13b782f163fdb892ea6749785886d05a336282fa659d722c7e92b);
end-to-end verified in Theseus
2026-08-18 20:14:43 +02:00
hermesState = {
skHex : Buffer . from ( sk ) . toString ( "hex" ) ,
pkHex , npub ,
inbox : [ ] ,
relays : new Map ( ) ,
_seen : new Set ( ) ,
} ;
for ( const url of HERMES _DEFAULT _RELAYS ) {
hermesState . relays . set ( url , await hermesConnectRelay ( url ) ) ;
}
// Give sockets a beat to open before reporting connected count.
await new Promise ( ( r ) => setTimeout ( r , 400 ) ) ;
let connected = 0 ;
for ( const s of hermesState . relays . values ( ) ) if ( s . ready ) connected ++ ;
2026-08-19 00:38:52 +02:00
return hOk ( { npub , pkHex , source : opts . useVault ? "vault" : "mnemonic" ,
relaysConnected : connected , relaysTotal : hermesState . relays . size } ) ;
Hermes: wire NIP-17 messaging into Theseus, provision chipnet hermes.bch
- Hermes/proof/: standalone keystone — BIP-39→Nostr, NIP-17 wrap/unwrap,
name-addressed send/receive verified end-to-end via public relay
- Argus/src/lib/hermes-derive.js: HKDF(silentmode/messenger/0) using libauth
so the registrar can compute np without pulling nostr-tools into Argus
- Argus/src/register-hermes-chipnet.mjs: idempotent REG/UPD script; publishes
np (Nostr pubkey) and nr (relay) records for a chipnet name
- TheseusNavigator/lib/hermes.js: same derivation + wrap/unwrap + record
parsing, canonical for Theseus; guarded by lib/package.json type:module
- TheseusNavigator/messages.html + messages-preload.js: Messages panel UI
(identity from mnemonic, live inbox, compose by .bch name), .bch suffix
stripped from displayed names
- TheseusNavigator/main.js: HERMES_MOD + loadHermesLib next to VAULT_MOD;
ipc handlers (hermes-status/init/close/inbox/send/open), per-relay
subscription with auto-reconnect, status pushed on WS open/close,
reverse-resolve pk -> .bch name via cached BNS index, Ctrl+Shift+M shortcut
via web-contents-created (works from any tab)
- Chipnet hermes.bch registered with np=f2c92519...67f4 nr=wss://nos.lol
(txid 7fc6de4544c13b782f163fdb892ea6749785886d05a336282fa659d722c7e92b);
end-to-end verified in Theseus
2026-08-18 20:14:43 +02:00
} catch ( e ) { hermesTeardown ( ) ; return hErr ( e ) ; }
} ) ;
2026-08-19 00:38:52 +02:00
// Cheap query: "is the vault-bound sign-in path available right now?" Panel
// uses this to decide whether to show the 'Use password vault' button.
ipcMain . handle ( "hermes-can-use-vault" , ( ) => hOk ( {
available : ! ! ( vaultState && vaultState . messengerRoot ) ,
} ) ) ;
Hermes: wire NIP-17 messaging into Theseus, provision chipnet hermes.bch
- Hermes/proof/: standalone keystone — BIP-39→Nostr, NIP-17 wrap/unwrap,
name-addressed send/receive verified end-to-end via public relay
- Argus/src/lib/hermes-derive.js: HKDF(silentmode/messenger/0) using libauth
so the registrar can compute np without pulling nostr-tools into Argus
- Argus/src/register-hermes-chipnet.mjs: idempotent REG/UPD script; publishes
np (Nostr pubkey) and nr (relay) records for a chipnet name
- TheseusNavigator/lib/hermes.js: same derivation + wrap/unwrap + record
parsing, canonical for Theseus; guarded by lib/package.json type:module
- TheseusNavigator/messages.html + messages-preload.js: Messages panel UI
(identity from mnemonic, live inbox, compose by .bch name), .bch suffix
stripped from displayed names
- TheseusNavigator/main.js: HERMES_MOD + loadHermesLib next to VAULT_MOD;
ipc handlers (hermes-status/init/close/inbox/send/open), per-relay
subscription with auto-reconnect, status pushed on WS open/close,
reverse-resolve pk -> .bch name via cached BNS index, Ctrl+Shift+M shortcut
via web-contents-created (works from any tab)
- Chipnet hermes.bch registered with np=f2c92519...67f4 nr=wss://nos.lol
(txid 7fc6de4544c13b782f163fdb892ea6749785886d05a336282fa659d722c7e92b);
end-to-end verified in Theseus
2026-08-18 20:14:43 +02:00
ipcMain . handle ( "hermes-close" , ( ) => { hermesTeardown ( ) ; return hOk ( ) ; } ) ;
ipcMain . handle ( "hermes-inbox" , ( ) => {
if ( ! hermesState ) return hErr ( "not initialised" ) ;
return hOk ( { messages : hermesState . inbox . slice ( ) } ) ;
} ) ;
// Send: `to` is either a 64-char hex pubkey or a .bch name. Names are resolved
// via the portable resolver (getResolver above), the `np` record parsed, then
// wrapped + published to each relay listed in `nr` (falling back to defaults).
ipcMain . handle ( "hermes-send" , async ( _e , { to , text } = { } ) => {
if ( ! hermesState ) return hErr ( "not initialised" ) ;
if ( ! to || ! text ) return hErr ( "to and text required" ) ;
try {
const H = await loadHermesLib ( ) ;
let recipientPkHex ; let relays = HERMES _DEFAULT _RELAYS . slice ( ) ;
const trimmed = String ( to ) . trim ( ) ;
if ( /^[0-9a-f]{64}$/i . test ( trimmed ) ) {
recipientPkHex = trimmed . toLowerCase ( ) ;
} else if ( trimmed . startsWith ( "npub1" ) ) {
recipientPkHex = H . parseNpRecord ( trimmed ) ;
} else {
const R = await getResolver ( ) ;
const name = R . normalizeName ( trimmed ) ;
const entry = await R . resolveName ( name , { WebSocket } ) ;
if ( ! entry ) return hErr ( ` no on-chain registration for ${ name } ` ) ;
const parsed = H . parseHermesRecords ( entry , { defaultRelays : HERMES _DEFAULT _RELAYS } ) ;
recipientPkHex = parsed . npPk ;
relays = parsed . relays ;
}
const sk = Buffer . from ( hermesState . skHex , "hex" ) ;
const wrap = H . wrapChat ( { senderSk : sk , recipientPkHex , text } ) ;
const publishOne = ( url ) => new Promise ( ( resolve ) => {
const ws = new WebSocket ( url ) ;
let settled = false ;
const done = ( r ) => { if ( settled ) return ; settled = true ; try { ws . close ( 1000 ) ; } catch { } resolve ( r ) ; } ;
const t = setTimeout ( ( ) => done ( { url , ok : false , detail : "timeout" } ) , 8000 ) ;
ws . on ( "open" , ( ) => ws . send ( JSON . stringify ( [ "EVENT" , wrap ] ) ) ) ;
ws . on ( "message" , ( buf ) => {
let msg ; try { msg = JSON . parse ( buf . toString ( ) ) ; } catch { return ; }
if ( msg [ 0 ] === "OK" && msg [ 1 ] === wrap . id ) {
clearTimeout ( t ) ;
done ( { url , ok : ! ! msg [ 2 ] , detail : msg [ 3 ] || "" } ) ;
}
} ) ;
ws . on ( "error" , ( e ) => done ( { url , ok : false , detail : "ws error: " + e . message } ) ) ;
} ) ;
const results = await Promise . all ( relays . map ( publishOne ) ) ;
const accepted = results . filter ( ( r ) => r . ok ) . length ;
return hOk ( { recipientPkHex , accepted , total : results . length , results } ) ;
} catch ( e ) { return hErr ( e ) ; }
} ) ;
function openHermesWindow ( ) {
if ( hermesWin && ! hermesWin . isDestroyed ( ) ) {
hermesWin . focus ( ) ; return ;
}
hermesWin = new BrowserWindow ( {
width : 720 , height : 620 , title : "Messages — Theseus" ,
backgroundColor : "#0b0e14" ,
webPreferences : {
preload : path . join ( _ _dirname , "messages-preload.js" ) ,
contextIsolation : true , nodeIntegration : false ,
} ,
} ) ;
hermesWin . setMenuBarVisibility ( false ) ;
hermesWin . loadFile ( "messages.html" ) ;
hermesWin . on ( "closed" , ( ) => { hermesWin = null ; } ) ;
}
ipcMain . handle ( "hermes-open" , ( ) => { openHermesWindow ( ) ; return { ok : true } ; } ) ;
Ship Theseus 0.0.8: window.bcnr dApp API + eTLD+1 permission origins
Merges a parallel session's work with the multi-source BNS story from 0.0.7.
The dApp side (parallel session)
--------------------------------
* bcnr-preload.js — installs `window.bcnr` on every page via contextBridge.
Read-only surface: resolveName(name), isRegistered(name), getBcnrTlds(),
getRecordVersion(name), plus getPermissionOrigin() for diagnostics. All
Promises; a missing name returns null (not throw). No signing, no wallet
unlock — that surface is designed but deliberately out of scope for 0.0.8
(see TheseusNavigator/DESIGN-integrated-wallet.md).
* bcnr-origin.js — pure function that computes the eTLD+1 permission origin
for a URL. ICANN suffixes via `psl` (same PSL Chromium uses, handles
.co.uk / .github.io / etc); BNS names key off the on-chain TLD list so
foo.wallet becomes a public suffix as soon as `wallet` appears there.
Match browser cookie / MetaMask semantics: a grant on pay.merchant.com
covers account.merchant.com but not evil.com.
* dev/bcnr-selftest.js, dev/origin-selftest.mjs — self-tests, no I/O.
* main.js wires bcnr-preload.js into session.defaultSession.setPreloads() so
it runs BEFORE per-WebContentsView preloads; adds bcnr:* IPC handlers.
* preload.js + chrome.html — small hooks so the shell picks up window.bcnr
the same way regular content does.
* package.json — psl dep, bcnr-preload.js/bcnr-origin.js in `files`.
Also included
-------------
* AriadneResolver/mobile/.../UpdateCheck.java — in-app update-check for the
Android app; already active in the shipped 0.11 APK (build.ps1 -Recurse
picked it up), formalising the source now.
* TheseusNavigator/snapshots/bns-name-snapshot.json — refreshed bundled
starter (73 beacon txs, root c37b8596…c54e414ba).
* Site pages + manifest updated to point at 0.0.8.
TheseusNavigator-Setup-0.0.8.exe 95.4 MB
21939743eafdfe8742a6b7c4b987bd2782384d7bc41289cb80a7e08019dc9f02
TheseusNavigator-0.0.8-portable.exe 92.7 MB
2aa429fe39dc0fa4ac040fc6d6eb31b0f890c8a83175c49fcb50f052c480d39d
2026-08-31 01:38:55 +02:00
// --- BCNR provider (window.bcnr) — read-only surface. See
// DESIGN-integrated-wallet.md §3. Every method reuses the resolver Theseus
// already runs; nothing about the user leaks, so no origin/permission gate.
// A malicious page can call these; the worst it learns is what the chain
// says publicly, which it could equally get through Argus. The preload that
// exposes these lives at bcnr-preload.js and is installed session-wide
// inside whenReady below.
//
// B.2b: eTLD+1 origin binding. The read methods below don't need the origin
// — chain data is public — so they don't compute it. Instead pages that
// want to see how Theseus will bucket their permissions can call
// `window.bcnr.getOrigin()` (handler further down). B.3's write methods
// (signMessage/sendPayment/registerName) will call `callerOrigin(event)` at
// entry and check the result against wallet-permissions.json.
const { originOf } = require ( "./bcnr-origin.js" ) ;
function callerOrigin ( event ) {
try { return originOf ( event . sender . getURL ( ) , { bcnrTlds } ) ; }
catch { return null ; }
}
// wallet-permissions.json — per-origin (eTLD+1) grants for B.3's write
// methods. Scaffolded in B.2b so B.3 doesn't have to touch main.js's
// on-disk conventions. Shape is intentionally open — B.3 will define the
// concrete decision values ("always" | "once" | "never", amount caps,
// expiries) as each write method lands.
let walletPermissions = { } ;
const walletPermissionsFile = ( ) => path . join ( app . getPath ( "userData" ) , "wallet-permissions.json" ) ;
function loadWalletPermissions ( ) {
try {
if ( fs . existsSync ( walletPermissionsFile ( ) ) ) {
const raw = JSON . parse ( fs . readFileSync ( walletPermissionsFile ( ) , "utf8" ) ) ;
if ( raw && typeof raw === "object" ) walletPermissions = raw ;
}
} catch ( e ) { console . error ( "wallet-permissions load failed:" , e . message ) ; }
}
function saveWalletPermissions ( ) {
try { fs . writeFileSync ( walletPermissionsFile ( ) , JSON . stringify ( walletPermissions , null , 2 ) ) ; }
catch ( e ) { console . error ( "wallet-permissions save failed:" , e . message ) ; }
}
// B.3 will use these. Kept here so the storage owner is one place.
function getWalletPermission ( origin , method ) {
if ( ! origin || ! method ) return null ;
return walletPermissions [ origin ] ? . [ method ] ? ? null ;
}
function setWalletPermission ( origin , method , value ) {
if ( ! origin || ! method ) return ;
if ( ! walletPermissions [ origin ] ) walletPermissions [ origin ] = { } ;
walletPermissions [ origin ] [ method ] = value ;
saveWalletPermissions ( ) ;
}
void getWalletPermission ; void setWalletPermission ; // silence unused-in-B.2b
function serializeEntry ( entry ) {
if ( ! entry ) return null ;
// Explicit whitelist — records/category/txid/height are the on-chain facts
// the design's `resolveName` promises. `updatedTxid` is included because it
// shifts every UPD and lets `getRecordVersion` distinguish a REG-only entry
// from one that has been updated in place.
return {
name : entry . name ,
category : entry . category ,
records : entry . records ? ? { } ,
txid : entry . txid ,
height : entry . height ,
updatedTxid : entry . updatedTxid ? ? null ,
} ;
}
ipcMain . handle ( "bcnr:resolveName" , async ( _e , name ) => {
if ( typeof name !== "string" || ! name ) return null ;
try { return serializeEntry ( await resolveHost ( name ) ) ; }
catch { return null ; }
} ) ;
ipcMain . handle ( "bcnr:isRegistered" , async ( _e , name ) => {
if ( typeof name !== "string" || ! name ) return false ;
try { return ( await resolveHost ( name ) ) != null ; }
catch { return false ; }
} ) ;
ipcMain . handle ( "bcnr:getBcnrTlds" , ( ) => bcnrTlds . slice ( ) ) ;
// Diagnostic — returns the eTLD+1 permission origin Theseus computes for the
// caller. Same value B.3's write methods will gate on. No leak: a page can
// already read its own location.href; this just tells it how Theseus buckets
// its permissions (so a dApp dev can see that pay.foo.bch and blog.foo.bch
// share one grant).
ipcMain . handle ( "bcnr:getOrigin" , ( e ) => callerOrigin ( e ) ) ;
ipcMain . handle ( "bcnr:getRecordVersion" , async ( _e , name ) => {
if ( typeof name !== "string" || ! name ) return null ;
try {
const entry = await resolveHost ( name ) ;
if ( ! entry ) return null ;
// The pair (updatedTxid ?? txid, height) uniquely identifies which reveal
// a dApp is looking at. dApps poll this cheaply and re-fetch records only
// when it changes.
return {
txid : entry . updatedTxid ? ? entry . txid ,
regTxid : entry . txid ,
height : entry . height ,
} ;
} catch { return null ; }
} ) ;
2026-08-31 02:50:57 +02:00
// App-level keyboard shortcuts. One `web-contents-created` hook covers
// every WebContents (chrome, tab views, overlays) without per-view wiring.
// Reload/hard-reload always target the active tab, regardless of which
// view received the key (URL bar focused, overlay focused, etc.), so the
// user's mental model matches every browser they've ever used.
Hermes: wire NIP-17 messaging into Theseus, provision chipnet hermes.bch
- Hermes/proof/: standalone keystone — BIP-39→Nostr, NIP-17 wrap/unwrap,
name-addressed send/receive verified end-to-end via public relay
- Argus/src/lib/hermes-derive.js: HKDF(silentmode/messenger/0) using libauth
so the registrar can compute np without pulling nostr-tools into Argus
- Argus/src/register-hermes-chipnet.mjs: idempotent REG/UPD script; publishes
np (Nostr pubkey) and nr (relay) records for a chipnet name
- TheseusNavigator/lib/hermes.js: same derivation + wrap/unwrap + record
parsing, canonical for Theseus; guarded by lib/package.json type:module
- TheseusNavigator/messages.html + messages-preload.js: Messages panel UI
(identity from mnemonic, live inbox, compose by .bch name), .bch suffix
stripped from displayed names
- TheseusNavigator/main.js: HERMES_MOD + loadHermesLib next to VAULT_MOD;
ipc handlers (hermes-status/init/close/inbox/send/open), per-relay
subscription with auto-reconnect, status pushed on WS open/close,
reverse-resolve pk -> .bch name via cached BNS index, Ctrl+Shift+M shortcut
via web-contents-created (works from any tab)
- Chipnet hermes.bch registered with np=f2c92519...67f4 nr=wss://nos.lol
(txid 7fc6de4544c13b782f163fdb892ea6749785886d05a336282fa659d722c7e92b);
end-to-end verified in Theseus
2026-08-18 20:14:43 +02:00
app . on ( "web-contents-created" , ( _event , wc ) => {
wc . on ( "before-input-event" , ( e , input ) => {
if ( input . type !== "keyDown" ) return ;
2026-08-31 02:50:57 +02:00
// Ctrl+Shift+M -> Messages panel
Hermes: wire NIP-17 messaging into Theseus, provision chipnet hermes.bch
- Hermes/proof/: standalone keystone — BIP-39→Nostr, NIP-17 wrap/unwrap,
name-addressed send/receive verified end-to-end via public relay
- Argus/src/lib/hermes-derive.js: HKDF(silentmode/messenger/0) using libauth
so the registrar can compute np without pulling nostr-tools into Argus
- Argus/src/register-hermes-chipnet.mjs: idempotent REG/UPD script; publishes
np (Nostr pubkey) and nr (relay) records for a chipnet name
- TheseusNavigator/lib/hermes.js: same derivation + wrap/unwrap + record
parsing, canonical for Theseus; guarded by lib/package.json type:module
- TheseusNavigator/messages.html + messages-preload.js: Messages panel UI
(identity from mnemonic, live inbox, compose by .bch name), .bch suffix
stripped from displayed names
- TheseusNavigator/main.js: HERMES_MOD + loadHermesLib next to VAULT_MOD;
ipc handlers (hermes-status/init/close/inbox/send/open), per-relay
subscription with auto-reconnect, status pushed on WS open/close,
reverse-resolve pk -> .bch name via cached BNS index, Ctrl+Shift+M shortcut
via web-contents-created (works from any tab)
- Chipnet hermes.bch registered with np=f2c92519...67f4 nr=wss://nos.lol
(txid 7fc6de4544c13b782f163fdb892ea6749785886d05a336282fa659d722c7e92b);
end-to-end verified in Theseus
2026-08-18 20:14:43 +02:00
if ( input . control && input . shift && ( input . key === "M" || input . key === "m" ) ) {
openHermesWindow ( ) ;
2026-08-31 02:50:57 +02:00
return e . preventDefault ( ) ;
}
2026-08-31 15:35:47 +02:00
// Ctrl+B -> toggle add-on sidebar (matches the VS Code convention).
// Silently no-ops if no add-on has registered a sidebar panel yet.
if ( input . control && ! input . shift && ! input . alt && ( input . key === "B" || input . key === "b" ) ) {
toggleSidebar ( ) ;
return e . preventDefault ( ) ;
}
2026-08-31 02:50:57 +02:00
// Reload: F5 or Ctrl+R soft; Ctrl+F5 or Ctrl+Shift+R hard (bypass cache).
// Chromium's built-in accelerators are unreliable once the app menu is
// null (Menu.setApplicationMenu(null) above), and none of them cover
// the hard variants anyway — so we wire all four explicitly.
const isR = input . key === "R" || input . key === "r" ;
const isF5 = input . key === "F5" ;
if ( isF5 || ( input . control && isR ) ) {
const t = activeTab ( ) ;
if ( t && ! t . settings ) {
const hard = ( input . control && input . shift && isR ) || ( input . control && isF5 ) ;
try {
if ( hard ) t . view . webContents . reloadIgnoringCache ( ) ;
else t . view . webContents . reload ( ) ;
} catch { }
}
return e . preventDefault ( ) ;
Hermes: wire NIP-17 messaging into Theseus, provision chipnet hermes.bch
- Hermes/proof/: standalone keystone — BIP-39→Nostr, NIP-17 wrap/unwrap,
name-addressed send/receive verified end-to-end via public relay
- Argus/src/lib/hermes-derive.js: HKDF(silentmode/messenger/0) using libauth
so the registrar can compute np without pulling nostr-tools into Argus
- Argus/src/register-hermes-chipnet.mjs: idempotent REG/UPD script; publishes
np (Nostr pubkey) and nr (relay) records for a chipnet name
- TheseusNavigator/lib/hermes.js: same derivation + wrap/unwrap + record
parsing, canonical for Theseus; guarded by lib/package.json type:module
- TheseusNavigator/messages.html + messages-preload.js: Messages panel UI
(identity from mnemonic, live inbox, compose by .bch name), .bch suffix
stripped from displayed names
- TheseusNavigator/main.js: HERMES_MOD + loadHermesLib next to VAULT_MOD;
ipc handlers (hermes-status/init/close/inbox/send/open), per-relay
subscription with auto-reconnect, status pushed on WS open/close,
reverse-resolve pk -> .bch name via cached BNS index, Ctrl+Shift+M shortcut
via web-contents-created (works from any tab)
- Chipnet hermes.bch registered with np=f2c92519...67f4 nr=wss://nos.lol
(txid 7fc6de4544c13b782f163fdb892ea6749785886d05a336282fa659d722c7e92b);
end-to-end verified in Theseus
2026-08-18 20:14:43 +02:00
}
} ) ;
} ) ;
2026-07-29 13:54:34 +02:00
// 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 ( ) ;
Theseus UX batch: tabs, address history, password autofill MVP, home cards
Six user-visible improvements + supporting infra, all uncommitted from
the earlier session-in-progress state. Ships together in one release.
Chrome / tabs
- Same-size tabs: flex 1 1 0 with max 200px, min 60px. Container gets
overflow: hidden so many tabs shrink evenly instead of scrolling out.
- Drag-and-drop tab reordering. HTML5 drag events on each .tab; drop
side chosen by pointer x within target (Chrome UX). New move-tab IPC
splices the tabs array + re-emits.
Address bar
- Persistent history at userData/history.json capped at 500 LRU. Ranked
by host-prefix > url-prefix > contains > title-contains > recency.
- Floating suggestions dropdown (addressPicker WebContentsView) anchored
under the URL bar. Debounced 80ms input; ArrowUp/Down forward to the
picker via address-cursor IPC; Enter fires goURL; blur closes after
160ms so click-through registers. New files address-picker.html +
address-picker-preload.js. Cleared by existing clearHistoryOnQuit.
Password autofill (A.2 MVP)
- Green key chip in the address bar appears when the vault is UNLOCKED
and the active tab's host has matching credentials (exact hostname
match for phase 1; eTLD+1 upgrade queued as A.2.5).
- Click chip → floating picker of usernames. Click a match → main.js
runs a small script in the active tab: finds first visible
input[type=password]:not([disabled]), walks the same form for a
visible text/email/tel/url/search input whose name/id/autocomplete
matches /username|user|email|login|account|id/, fills both via the
native value setter + dispatches input/change so React/Vue-controlled
inputs update. New files pw-fill.html + pw-fill-preload.js.
- emitPwAvailability fires from pushNav + vault setup/unlock/lock so
the chip's visibility + count stays accurate.
Bookmarks bar
- Right-click context menu on the favorites bar. On empty area:
"Add current page" (or "Remove current page" if already saved). On a
specific bookmark: "Open", "Edit title…" (prompt), "Remove", plus
the add/remove-current entry. Uses a shared .ctxmenu style mirroring
the settings ctxmenu (dark/light aware).
- Empty-state text updated to mention right-click.
Home page
- Larger responsive card grid: auto-fill minmax(260-280px, 1fr) with
breakpoints at 600/900/1200. Cards have a subtitle line, a colored
badge (on-chain / Sia / server / custom), and edit affordances that
reveal only in Edit mode.
- User-editable set: Edit toggle reveals per-card ✎/✕ + a dashed "+ Add
card" tile. Modal for add/edit with title / URL / subtitle / badge.
Reset-to-defaults button.
- Persisted at userData/home-cards.json. New home-preload.js exposes
window.home = { getCards, setCards, resetCards, navigate }. IPC
handlers in main.js validate sender.getURL() matches our own
home.html — third-party pages see the API shape via the preload but
can't act on the user's local cards.
- Fallback set of 2 cards renders when window.home is unavailable
(e.g. opening home.html directly outside Electron for preview) so
the grid is never blank.
Docs
- TheseusNavigator/ROADMAP-identity-wallet.md — the phased plan for
the two independent strands (password manager A.2/3, browser wallet
B.1-6). Committed earlier this session; re-listed here for context.
- TheseusNavigator/SESSION-PROMPT-identity-wallet.md — pastable
kickoff for the next session picking up either strand.
Files added to build.files: address-picker.html,
address-picker-preload.js, pw-fill.html, pw-fill-preload.js,
home-preload.js.
2026-08-17 02:17:12 +02:00
loadHistory ( ) ;
2026-08-02 11:39:00 +02:00
loadCollisions ( ) ;
Ship Theseus 0.0.8: window.bcnr dApp API + eTLD+1 permission origins
Merges a parallel session's work with the multi-source BNS story from 0.0.7.
The dApp side (parallel session)
--------------------------------
* bcnr-preload.js — installs `window.bcnr` on every page via contextBridge.
Read-only surface: resolveName(name), isRegistered(name), getBcnrTlds(),
getRecordVersion(name), plus getPermissionOrigin() for diagnostics. All
Promises; a missing name returns null (not throw). No signing, no wallet
unlock — that surface is designed but deliberately out of scope for 0.0.8
(see TheseusNavigator/DESIGN-integrated-wallet.md).
* bcnr-origin.js — pure function that computes the eTLD+1 permission origin
for a URL. ICANN suffixes via `psl` (same PSL Chromium uses, handles
.co.uk / .github.io / etc); BNS names key off the on-chain TLD list so
foo.wallet becomes a public suffix as soon as `wallet` appears there.
Match browser cookie / MetaMask semantics: a grant on pay.merchant.com
covers account.merchant.com but not evil.com.
* dev/bcnr-selftest.js, dev/origin-selftest.mjs — self-tests, no I/O.
* main.js wires bcnr-preload.js into session.defaultSession.setPreloads() so
it runs BEFORE per-WebContentsView preloads; adds bcnr:* IPC handlers.
* preload.js + chrome.html — small hooks so the shell picks up window.bcnr
the same way regular content does.
* package.json — psl dep, bcnr-preload.js/bcnr-origin.js in `files`.
Also included
-------------
* AriadneResolver/mobile/.../UpdateCheck.java — in-app update-check for the
Android app; already active in the shipped 0.11 APK (build.ps1 -Recurse
picked it up), formalising the source now.
* TheseusNavigator/snapshots/bns-name-snapshot.json — refreshed bundled
starter (73 beacon txs, root c37b8596…c54e414ba).
* Site pages + manifest updated to point at 0.0.8.
TheseusNavigator-Setup-0.0.8.exe 95.4 MB
21939743eafdfe8742a6b7c4b987bd2782384d7bc41289cb80a7e08019dc9f02
TheseusNavigator-0.0.8-portable.exe 92.7 MB
2aa429fe39dc0fa4ac040fc6d6eb31b0f890c8a83175c49fcb50f052c480d39d
2026-08-31 01:38:55 +02:00
loadWalletPermissions ( ) ;
2026-07-29 13:54:34 +02:00
applyPermissions ( ) ;
2026-08-05 18:38:38 +02:00
applyEmbedCookieShim ( ) ;
2026-07-29 13:54:34 +02:00
applyAcceptLanguage ( ) ;
Ship Theseus 0.0.8: window.bcnr dApp API + eTLD+1 permission origins
Merges a parallel session's work with the multi-source BNS story from 0.0.7.
The dApp side (parallel session)
--------------------------------
* bcnr-preload.js — installs `window.bcnr` on every page via contextBridge.
Read-only surface: resolveName(name), isRegistered(name), getBcnrTlds(),
getRecordVersion(name), plus getPermissionOrigin() for diagnostics. All
Promises; a missing name returns null (not throw). No signing, no wallet
unlock — that surface is designed but deliberately out of scope for 0.0.8
(see TheseusNavigator/DESIGN-integrated-wallet.md).
* bcnr-origin.js — pure function that computes the eTLD+1 permission origin
for a URL. ICANN suffixes via `psl` (same PSL Chromium uses, handles
.co.uk / .github.io / etc); BNS names key off the on-chain TLD list so
foo.wallet becomes a public suffix as soon as `wallet` appears there.
Match browser cookie / MetaMask semantics: a grant on pay.merchant.com
covers account.merchant.com but not evil.com.
* dev/bcnr-selftest.js, dev/origin-selftest.mjs — self-tests, no I/O.
* main.js wires bcnr-preload.js into session.defaultSession.setPreloads() so
it runs BEFORE per-WebContentsView preloads; adds bcnr:* IPC handlers.
* preload.js + chrome.html — small hooks so the shell picks up window.bcnr
the same way regular content does.
* package.json — psl dep, bcnr-preload.js/bcnr-origin.js in `files`.
Also included
-------------
* AriadneResolver/mobile/.../UpdateCheck.java — in-app update-check for the
Android app; already active in the shipped 0.11 APK (build.ps1 -Recurse
picked it up), formalising the source now.
* TheseusNavigator/snapshots/bns-name-snapshot.json — refreshed bundled
starter (73 beacon txs, root c37b8596…c54e414ba).
* Site pages + manifest updated to point at 0.0.8.
TheseusNavigator-Setup-0.0.8.exe 95.4 MB
21939743eafdfe8742a6b7c4b987bd2782384d7bc41289cb80a7e08019dc9f02
TheseusNavigator-0.0.8-portable.exe 92.7 MB
2aa429fe39dc0fa4ac040fc6d6eb31b0f890c8a83175c49fcb50f052c480d39d
2026-08-31 01:38:55 +02:00
// Session-wide preload for `window.bcnr` — runs BEFORE per-WebContentsView
// preloads (home/settings/popover/etc.), which stack on top of it. Must be
// called before any tab is created; whenReady runs before createWindow().
try {
const bcnrPreload = path . join ( _ _dirname , "bcnr-preload.js" ) ;
const existing = session . defaultSession . getPreloads ( ) ;
if ( ! existing . includes ( bcnrPreload ) ) {
session . defaultSession . setPreloads ( [ ... existing , bcnrPreload ] ) ;
}
} catch ( err ) { console . warn ( "[bcnr] setPreloads failed:" , err ? . message ? ? err ) ; }
2026-07-29 13:54:34 +02:00
protocol . handle ( "bns" , serveBns ) ;
2026-08-02 11:39:00 +02:00
installDownloadTracker ( ) ;
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
initAddons ( ) ;
2026-07-29 13:54:34 +02:00
createWindow ( ) ;
Theseus 0.0.7: multi-source BNS with continuous delta refresh
The first .bch page now opens with zero user-visible latency and stays fresh
for as long as the browser runs. Four sources conspire in parallel — none of
them can block a navigation:
1. Warm start from disk (sync)
warmFromSnapshot loads the user-cache snapshot first, then falls back to
the copy bundled with the build. sharedIndex is set BEFORE the first
navigation can even fire.
2. Continuous background delta refresh (every 30 s)
startBnsPolling opens one electrum connection, fetches the beacon
history (a single call), and only pulls verbose tx bodies for txids we
don't already have — mergeFreshHistory-style. Merges into memory,
rebuilds the index locally, persists the enlarged snapshot to disk.
Turns a ~60 s full walk into ~1 s per new event.
3. Sia snapshot pull on boot (one-shot, wins the NEXT boot)
refreshSnapshotFromSia downloads the operator's published snapshot from
s3.silentmode.st for the next launch. After a long idle period the
browser resumes from that fresher snapshot instead of walking days of
events.
4. ensureIndex full-walk fallback
Still runs on boot for the case where there's no bundled snapshot AND
the poll hasn't landed yet — very first launch, offline install, etc.
resolveHost now prefers sharedIndex (which the poll keeps live) and, on a
miss with a stale index, triggers pollAndMerge (delta fetch) instead of
ensureIndex (full walk). The old full-walk fallback is only taken if the
delta primitives aren't available (running against an older resolver-web).
Argus/src/lib/resolver-web.js exports connectElectrum so the Theseus poll
can drive its own ad-hoc queries against the pool without duplicating the
Electrum wrapper.
Bundled starter snapshot refreshed: 73 beacon txs, root c37b859682…54e414ba.
Artifacts:
dist-public/TheseusNavigator-Setup-0.0.7.exe (95.4 MB)
sha256 1b0dabcd2b13067aa1fd89c3271708b35ba22dfc7a620e248b35e0487b7a104f
dist-public/TheseusNavigator-0.0.7-portable.exe (92.7 MB)
sha256 706dc01b21a88d3c2fbc3eeced0cab6ace873630bf3edc2d40915ca5be5c8cae
2026-08-30 15:57:01 +02:00
// Multi-source BNS warm-up so the first .bch page opens near-instantly and
// stays fresh for as long as the browser is running. Every source runs in
// parallel — none of them can block a navigation.
// 1) sync: load the on-disk snapshot (user cache > bundled).
// sharedIndex is set BEFORE the first navigation can even fire.
// 2) async, continuous: startBnsPolling() opens one electrum connection
// every 30 s, fetches the beacon history (one call), and only pulls
// the tx bodies we don't already have. Merges into currentSnapshotState
// and persists — so on every page navigation the sharedIndex is at
// most 30 s old with zero user-visible latency.
// 3) async, one-shot: refresh the on-disk snapshot from the operator's
// Sia mirror. Wins the NEXT boot, not this one — after a long idle
// period the browser resumes from a snapshot fresher than the poll
// could catch up on quickly.
// 4) fallback: ensureIndex() still exists for the very first launch
// where the bundled snapshot is absent AND the poll hasn't landed
// yet — a full-walk build.
BNS: Sia+Nostr snapshot mirror, Theseus warm-start, VPS playbook, electrum.bch
Four independent pieces of one story — make cold-start .bch resolution fast,
survive individual electrum outages, and turn adding a Silent Mode BNS server
into a checklist run.
* Argus/src/lib/snapshot-name.js — pure, interpretation-free snapshot format
for the name beacon (raw history + verbose tx cache + deterministic root).
* Argus/src/publish-name-mirror.mjs — mirror the snapshot to Sia S3
(floating + content-addressed) and Nostr NIP-33 (kind 30078,
d-tag bns-name-list). Same pattern as publish-tld-mirror.mjs.
* Argus/src/snapshot-name-to-file.mjs — dump the snapshot to a file for
bundling. Writes to TheseusNavigator/snapshots/bns-name-snapshot.json.
* Argus/src/lib/resolver-web.js — new export buildIndexFromSnapshot() runs
the same REG/UPD reduction as buildIndex() but from a pre-fetched snapshot,
no electrum required. Warm-start path in Theseus depends on this.
* TheseusNavigator/main.js — warmFromSnapshot() and refreshSnapshotFromSia()
wired into app.whenReady(). First .bch navigation returns from a warm index
instead of waiting on a full electrum walk.
* TheseusNavigator/package.json — bundles the starter snapshot as an
extraResource so packaged builds ship with a floor.
* TheseusNavigator/snapshots/bns-name-snapshot.json — first starter snapshot
(70 beacon txs, root 8cc859aa…).
* VPS/BNS-SERVER-PLAYBOOK.md — turn-key checklist for spinning up a second
or third Silent Mode BNS server, distilled from what actually worked on
the silentmode box (BCHN 29 [chip]-not-[chipnet], multi-beacon indexer,
wss:// nginx, on-chain publish via electrum.bch, common gotchas table).
* Argus/src/register-electrum-list.mjs — publish the current chipnet
server pool as `el` on electrum.bch. Fits inside the 200-byte OP_RETURN
cap by sizing the record (drops fallback IP pins in favour of URL-only
tokens until they fit). NAME was chosen deliberately: two-part `electrum.bch`
normalises to the bare distinct name `electrum`, whereas the earlier
candidate `electrum.silentmode.bch` collapsed to silentmode.bch and would
have inherited/overwritten that name's records.
* Argus/src/revert-silentmode-bch.mjs — one-off recovery from the earlier
register-electrum-list revision that hit exactly that subdomain-collapse
bug (broadcast an UPD to silentmode.bch and clobbered its s3 record).
Records recovered from the local snapshot cache; safe to leave in the
tree as documentation of what happened.
2026-08-30 09:59:01 +02:00
warmFromSnapshot ( ) . catch ( ( ) => { } ) ;
Theseus 0.0.7: multi-source BNS with continuous delta refresh
The first .bch page now opens with zero user-visible latency and stays fresh
for as long as the browser runs. Four sources conspire in parallel — none of
them can block a navigation:
1. Warm start from disk (sync)
warmFromSnapshot loads the user-cache snapshot first, then falls back to
the copy bundled with the build. sharedIndex is set BEFORE the first
navigation can even fire.
2. Continuous background delta refresh (every 30 s)
startBnsPolling opens one electrum connection, fetches the beacon
history (a single call), and only pulls verbose tx bodies for txids we
don't already have — mergeFreshHistory-style. Merges into memory,
rebuilds the index locally, persists the enlarged snapshot to disk.
Turns a ~60 s full walk into ~1 s per new event.
3. Sia snapshot pull on boot (one-shot, wins the NEXT boot)
refreshSnapshotFromSia downloads the operator's published snapshot from
s3.silentmode.st for the next launch. After a long idle period the
browser resumes from that fresher snapshot instead of walking days of
events.
4. ensureIndex full-walk fallback
Still runs on boot for the case where there's no bundled snapshot AND
the poll hasn't landed yet — very first launch, offline install, etc.
resolveHost now prefers sharedIndex (which the poll keeps live) and, on a
miss with a stale index, triggers pollAndMerge (delta fetch) instead of
ensureIndex (full walk). The old full-walk fallback is only taken if the
delta primitives aren't available (running against an older resolver-web).
Argus/src/lib/resolver-web.js exports connectElectrum so the Theseus poll
can drive its own ad-hoc queries against the pool without duplicating the
Electrum wrapper.
Bundled starter snapshot refreshed: 73 beacon txs, root c37b859682…54e414ba.
Artifacts:
dist-public/TheseusNavigator-Setup-0.0.7.exe (95.4 MB)
sha256 1b0dabcd2b13067aa1fd89c3271708b35ba22dfc7a620e248b35e0487b7a104f
dist-public/TheseusNavigator-0.0.7-portable.exe (92.7 MB)
sha256 706dc01b21a88d3c2fbc3eeced0cab6ace873630bf3edc2d40915ca5be5c8cae
2026-08-30 15:57:01 +02:00
startBnsPolling ( ) ;
refreshSnapshotFromSia ( ) . catch ( ( ) => { } ) ;
ensureIndex ( ) . catch ( ( ) => { } ) ; // fallback for first launch without a bundled snapshot
Theseus 0.0.4: cheap in-app update-check chip
Bumps version so the chip actually surfaces itself on 0.0.3 installs
(the version-newer check requires a strict semver bump — same-version
rebuilds don't trigger the chip). From this release on, whenever the
manifest names a newer Theseus, users get a one-click download.
Mechanism
- main.js checkForUpdate() fetches https://dl.silentmode.st/releases-
manifest.json on startup (5s timeout, cache: no-store) + every 6h.
Finds the theseus-navigator release, compares version to
app.getVersion() with a numeric a.b.c comparator that handles
"0.10.0 > 0.9.9" correctly.
- On a match → stores { version, setupUrl, portableUrl, setupHash,
portableHash, date } and emits update-available to chrome. Cleared
after the user upgrades + relaunches (same-version → null).
- Re-emits on chrome's did-finish-load in case the fetch beats the
chrome view.
Chip UI (chrome.html)
- Acid-yellow pill between the downloads button and the Tor toggle:
"↓ Update to X.Y.Z" + a ✕. Main body opens setupUrl in the system
browser via shell.openExternal (origin-validated to
https://dl.silentmode.st/ or https://silentmode.st/). ✕ dismisses
for the current session — you'll see it again next launch if still
behind.
Trust anchor
- No signing / no cryptographic verification of the download in this
phase. releases.silentmode.bch publishes the SAME manifest URL, so
users who want to verify can cross-check the manifest hash against
what BCNR returns. The proper auto-updater with signature checks is
the follow-on to this cheap version.
Non-goals in phase 1
- No delta downloads; the user clicks and gets a full installer.
- No auto-install; download → user runs the installer themselves.
- No "check now" button in Settings; the periodic timer suffices.
- No portable-vs-installed detection; the chip prefers setupUrl (the
installer upgrades in place). Right-click for portable is future work.
2026-08-28 20:21:44 +02:00
// Cheap update check: fetch the releases manifest and, if a newer
// version is out, surface a chip in the toolbar. No auto-install —
// clicking the chip opens the download URL. Recheck every 6h so a
// browser left running for days catches updates without a relaunch.
checkForUpdate ( ) . catch ( ( ) => { } ) ;
setInterval ( ( ) => checkForUpdate ( ) . catch ( ( ) => { } ) , 6 * 60 * 60 * 1000 ) ;
2026-07-29 13:54:34 +02:00
app . on ( "activate" , ( ) => { if ( BrowserWindow . getAllWindows ( ) . length === 0 ) createWindow ( ) ; } ) ;
} ) ;
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
app . on ( "before-quit" , async ( e ) => {
// Auto-clear per user settings. saveSession() runs first so restoreSession
// still works UNLESS the user asked to drop history — in which case we
// wipe the session file too so the next launch is genuinely blank.
saveSession ( ) ;
stopTor ( ) ;
Theseus 0.0.7: multi-source BNS with continuous delta refresh
The first .bch page now opens with zero user-visible latency and stays fresh
for as long as the browser runs. Four sources conspire in parallel — none of
them can block a navigation:
1. Warm start from disk (sync)
warmFromSnapshot loads the user-cache snapshot first, then falls back to
the copy bundled with the build. sharedIndex is set BEFORE the first
navigation can even fire.
2. Continuous background delta refresh (every 30 s)
startBnsPolling opens one electrum connection, fetches the beacon
history (a single call), and only pulls verbose tx bodies for txids we
don't already have — mergeFreshHistory-style. Merges into memory,
rebuilds the index locally, persists the enlarged snapshot to disk.
Turns a ~60 s full walk into ~1 s per new event.
3. Sia snapshot pull on boot (one-shot, wins the NEXT boot)
refreshSnapshotFromSia downloads the operator's published snapshot from
s3.silentmode.st for the next launch. After a long idle period the
browser resumes from that fresher snapshot instead of walking days of
events.
4. ensureIndex full-walk fallback
Still runs on boot for the case where there's no bundled snapshot AND
the poll hasn't landed yet — very first launch, offline install, etc.
resolveHost now prefers sharedIndex (which the poll keeps live) and, on a
miss with a stale index, triggers pollAndMerge (delta fetch) instead of
ensureIndex (full walk). The old full-walk fallback is only taken if the
delta primitives aren't available (running against an older resolver-web).
Argus/src/lib/resolver-web.js exports connectElectrum so the Theseus poll
can drive its own ad-hoc queries against the pool without duplicating the
Electrum wrapper.
Bundled starter snapshot refreshed: 73 beacon txs, root c37b859682…54e414ba.
Artifacts:
dist-public/TheseusNavigator-Setup-0.0.7.exe (95.4 MB)
sha256 1b0dabcd2b13067aa1fd89c3271708b35ba22dfc7a620e248b35e0487b7a104f
dist-public/TheseusNavigator-0.0.7-portable.exe (92.7 MB)
sha256 706dc01b21a88d3c2fbc3eeced0cab6ace873630bf3edc2d40915ca5be5c8cae
2026-08-30 15:57:01 +02:00
stopBnsPolling ( ) ; // silence the background delta refresh before exit
Snapshot in-progress work: Ariadne mobile, Theseus password manager, Hephaestus
Several concurrent workstreams committed together as a checkpoint:
- Ariadne mobile resolver — BchFetcher/Bns/MainActivity resolution logic,
AndroidManifest + build.ps1
- Theseus password manager — settings.html/chrome.html/settings-preload.js UI +
main.js wiring + package.json resource; Argus password-vault.js, record-picker.js
(+ tests) and resolver-web.d.ts
- Hephaestus — new BCH-wallet OIDC auth-proxy + Forgejo docker-compose and
restic/S3 scripts (secrets referenced via env only; Hephaestus/.env is gitignored)
- Argus public-gateway.mjs updates
- Docs — root README, Email README/RUNBOOK, VPS access runbooks (Checkers/Deviant),
site/hermes, WebsiteDev registry + faster-blocks, Failures/ AAAA-mangle writeup,
Decentralized Storage map, coordination notes
- .gitignore — exclude /.keys/ and Hephaestus/.env
2026-08-14 23:17:18 +02:00
vaultState = null ; // drop the in-memory vault key + purposeRoot
try {
await clearBrowsingData ( {
cookies : settings . clearCookiesOnQuit ,
cache : settings . clearCacheOnQuit ,
storage : settings . clearStorageOnQuit ,
} ) ;
if ( settings . clearHistoryOnQuit ) {
try { fs . unlinkSync ( sessionFile ( ) ) ; } catch { }
}
} catch ( err ) { console . error ( "before-quit clear failed:" , err ? . message ) ; }
} ) ;
2026-07-29 13:54:34 +02:00
app . on ( "window-all-closed" , ( ) => { stopTor ( ) ; if ( process . platform !== "darwin" ) app . quit ( ) ; } ) ;
}
module . exports = { serveBns , resolveHost , isBnsHost , nativeTld , dualTld , registryOf } ;