Commit graph

35 commits

Author SHA1 Message Date
Local Dev
99a5923f42 Theseus: tell add-ons which name system served the page
A page message carried only an origin, and a bns:// page is shown as
https://. The same name can be one site on BNS and another on the ordinary
web (BNS carries registrations under ICANN TLDs on purpose), so an add-on
that files permissions by origin could not tell the two apart. The call
context now carries `registry: "bns" | "web"`, also across the sandbox
boundary, and api.features.pageRegistry says the host provides it.
2026-10-05 23:40:14 +02:00
Local Dev
7cc66ce680 Theseus: community extensions run in their own sandboxed process
An extension was require()d into the browser's main process. Its declared
capabilities bound only an honest one: there it could load electron, hook
the unlock prompt for the master password, read the vault files and the
wallet's store, call the OS keystore, and reach every tab.

Extensions that do not ship with Theseus now run in a separate process, in
Node mode under Node's permission model: read access to their own folder,
write access to a scratch folder, no child processes, no workers, no native
add-ons, no Electron, and none of the browser's memory. They reach the
browser only through an allow-listed message API that calls the same
functions, with the same capability checks, as before. Built-in add-ons are
unchanged and stay in-process.

For extension authors: host calls return promises (tabs.active included);
api.require / api.import are gone, so dependencies must be bundled;
registerRequestFilter and registerSiteRoute are not offered; the store is
handed over at start and written through, so storage.get stays synchronous.
An approval raised while handling a page message is still tied to that
page's tab. If the sandbox cannot start, the extension does not run.

The channel is JSON with tagged bytes: the binary IPC format depends on the
exact V8 build on both ends.
2026-10-05 22:54:26 +02:00
Local Dev
99d7684590 Theseus: an extension update applied in place reloads its open panels
Applying an extension update without a restart swapped the files and
restarted the add-on, but a panel or tab that was already open kept
running the previous version's page, so the extension looked unchanged
until Theseus restarted (Pithos showed its old layout this way). The
add-on's open left panel, sidebar panel and tabs now reload after the
swap. Extensions can also apply their own staged update with
api.applySelfUpdate(), so their own update button needs no restart;
plug-ins such as wallets still update on the next launch.
2026-10-04 15:28:25 +02:00
Local Dev
f834fbf80a Theseus: a wallet approval belongs to the tab that asked for it
Approvals were one global queue drawn over whatever tab was in front,
so a background tab could time a request to pop up while the user was
in the middle of something on a trusted dapp, and a request outlived
the page that made it. The host now carries the tab of a page message
through the add-on's handler (AsyncLocalStorage across its awaits); an
approval from a page shows only while its tab is in front, hides and
comes back re-armed when the user switches tabs, and is cancelled when
the tab closes or navigates. Approvals from a panel or from
WizardConnect have no tab and behave as before.
2026-10-04 04:28:18 +02:00
Local Dev
9d6d8c3cc5 Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.

The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
Local Dev
bdc8f951c6 Theseus: built-in extensions can answer paths under a name they ship with
Pithos needs its full-page app at pithos.sia/<user>/<drive>/<folder>
instead of a loopback address. A new site-route capability lets a
built-in add-on declare names in its manifest (siteRoutes) and register
a handler for them; the bns:// handler asks it first for every path but
the root, and a handler that returns nothing hands the request back to
the name's own site. Community add-ons cannot use it, since answering
for a name is impersonating it.
2026-10-04 03:29:30 +02:00
Local Dev
80fe82abee Theseus: an add-on can limit which sites get its bridge; vault secrets stay first-party
- Page-inject policy. An add-on may keep { mode, origins } under the
  reserved storage key "__pageInjectPolicy"; in "allowed" mode its bridge is
  injected, and its page messages accepted, only on the listed origins. It
  is read from the store because the decision is made synchronously at
  document start and must hold while an on-demand add-on is still dormant.
  A wallet injected into every page tells every page the user has one.
  api.features.pageInjectPolicy lets an add-on tell an old host apart.
- vault.imports (raw imported seeds and keys, not namespaced per add-on) and
  vault.lifecycle unlock/setup/lock (the master password, and an
  unthrottled oracle for it) are for add-ons that ship with Theseus. A
  community extension with vault-derive could read the wallet's imported
  keys. Others use vault.requestUnlock, where Theseus draws the prompt.
2026-10-04 01:55:40 +02:00
Local Dev
2dfa9e9c3e Theseus: an add-on's key namespace cannot be taken by another add-on
- `absorbs` lets one add-on derive under another's vault namespace. A
  community install has the field stripped, but an update of one is placed
  as shipped, so a second version could declare absorbs:["aegis"] and derive
  the wallet's keys. It is now honoured only for ids that ship inside
  Theseus, at the one place that matters: vault.derive.
- The legacy ids Aegis absorbs ("bchwallet", "siawallet") no longer have a
  bundled folder, so nothing stopped a catalog extension from installing
  under one and deriving `bchwallet/...`. They are reserved at install and
  refused at derive.
- A page-to-add-on message is accepted only from the tab's top-level frame.
  The origin shown to the user is the top-level URL, so a subframe that
  reached the channel would have been credited with its parent's origin.
- The approval overlay ignores everything but Cancel for the first 800 ms.
  A page can raise it without a gesture and knows where the primary button
  lands, which made "double-click here" a way to approve a spend.
2026-10-04 01:41:01 +02:00
Local Dev
278da658ce Merge extensions-on-first-use: add-ons start when first used, not at launch
The left-edge panels landed meanwhile, so a panel record now carries both
its side and replace-by-id; a manifest-declared panel may say side:left too,
or a dormant add-on would show its panel on the wrong edge until it starts.
2026-10-03 21:07:44 +02:00
Local Dev
7087ab57ca Theseus: extension panels on the left edge
Extensions could only put panels in the right sidebar. An add-on can now
register a panel with side: "left": its icon joins the quick-links strip
(above the web-app links), and it opens in the strip's panel slot in its
own view with the sidebar preload, so the add-on bridge, events and the
widen/narrow controls work as on the right. A left panel and a quick-link
web app never share the slot; reopening keeps the panel's page and state.
Left panels drop out of the right sidebar and its dock. With the strip
turned off they fall back to the right sidebar so they stay reachable.

Pithos is the first: bundled copy rebuilt with side: "left".
2026-10-03 20:48:55 +02:00
Local Dev
de7735feb3 Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:

- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
  wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
  and the result is sealed with the OS keystore (safeStorage: DPAPI /
  Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
  elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
  lives in the same file, so a restart does not reset it; a successful
  master-password unlock does. A PIN whose password no longer opens the
  vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
  the master password. Extensions call api.vault.requestUnlock({ reason })
  (vault-derive capability) and get { ok } back; what the user typed never
  reaches them. Settings' locked screen offers "Unlock with PIN" through
  the same prompt.
2026-10-03 20:33:26 +02:00
Local Dev
65ef59f5c8 Theseus: add-on stores live in memory — no more 16 s "Not Responding" at launch
An add-on's storage.get read and parsed its whole store file on every call,
and storage.set read, parsed and rewrote it — synchronously, on the main
thread. Traced on a real profile (installed 0.3.70): with a 7.5 MB Aegis
store, 30 of the first 35 s of main-thread time went to storage.get, the
window sat in "Not Responding" from 3 s to 19 s, and the first page showed at
19 s. One get cost ~73 ms; Aegis does dozens per state update.

lib/addon-store.cjs keeps one in-memory copy per store, shared by the
add-on's api.storage (addons-host.js) and its pages (addon-storage-* IPC in
main.js). After a one-time load a get costs microseconds; values are copied
in and out (structuredClone), so callers keep the old semantics. Writes are
coalesced (100 ms) and land as temp-file + rename, and are flushed on quit;
a store that doesn't parse is moved aside instead of being replaced by {}.

Measured on copies of the same profile, dev build:
  first page 16.9-17.6 s -> 1.5-1.7 s; main thread blocked 24.7-25.8 s of
  30 -> 1.0-1.1 s; longest freeze 13.7-15.0 s -> 0.6 s.

The Aegis side (capping its unbounded txCache) ships separately through
Aegis's own update channel. The boot tracer gains total/longest block columns.
2026-10-03 16:08:22 +02:00
Local Dev
4cc2d53582 Theseus: start extensions on first use, Startup switches in Settings
Every enabled add-on used to be activated synchronously in initAddons(),
during app.whenReady and before the window exists. That is the largest
launch cost left (boot tracer, 2026-10-03). An add-on can now say
"activation": "on-demand" in addon.json. It is then listed at launch but
not started. Its declared surfaces stay live: "panels" (new: sidebar
panels declared up front), toolbar-menu, context-menu-items and the
page-inject bridge, whose source is read on the first matching page.
activate() runs on first real use: a panel opened, a menu or context
item picked, a message from its panel, tab or page bridge, a Settings
addon-invoke, a wiz:// link (for Aegis). All of those go through
AddonHost.dispatch(), which starts the add-on and waits for it, so no
call is dropped. Concurrent callers share one activation, and
activations run one at a time.

Startup stays the default: the host cannot tell what an older add-on
does in activate(). request-filter add-ons and add-ons that declare no
surface are forced to startup. If activate() returns a promise, calls
wait for it (at most 5 s). api.startAtLaunch(bool) lets an on-demand
add-on ask to be started at launch again (for live relay sessions).

Converted: notepad, screenshot, translate, docx-editor, pdf-editor, vpn
(none has launch-time work: no file association, no auto-connect, and
add-on file tabs are not part of the saved session). Shield and Cookie
Pop-ups stay startup: Shield owns the request filter and must see the
first request; Cookie Pop-ups costs ~6 ms and acts unasked on every
page. Aegis stays startup and untouched: another session owns it. See
NOTE-aegis-on-demand.md (next commit).

Settings › Performance › Startup:
- "Start extensions when first used" (default on). Off = all at launch;
  switching it off starts the waiting add-ons immediately.
- "Start the wallet at launch". Shown disabled with a hint until the
  installed Aegis manifest allows on-demand. It applies with no Settings
  change once Aegis opts in.
- "Preload common menus" gates prewarmOverlays().
- "Use lightest" preset.
New keys are plain SETTINGS_DEFAULTS through the existing settings-set.
No new IPC channels.

Measured: boot-trace, fresh profile, --seconds 20 so the 30 s add-on OTA
poll can't swap Aegis mid-series; warm runs 2-3 of two paired series.
- Add-on activation at launch: 121-154 ms -> 104-174 ms. The six
  converted add-ons went from 16-20 ms to 0. The rest is Shield (83-148
  ms, noisy) and Aegis (15 ms in this tree's 0.9.0).
- Toolbar painted: 1278-1584 ms -> 1282-1481 ms (within noise).
- With "Preload common menus" off: 0 overlays prewarmed, 8 processes
  instead of 11, about 50-70 MB less at 15 s.

The bundled add-on versions are not bumped. Existing profiles keep their
old addon.json, and so stay on startup activation, until those add-ons
ship with a higher version (seedBundledAddons only reseeds a strictly
newer bundle).
2026-10-03 16:05:32 +02:00
Local Dev
08beadcf8f Theseus: close the Settings-tab vault leak and the add-on update signer bypass
A preload belongs to the WebContents, not the page: a website loaded into
the Settings tab kept window.cfg and could read every vault password, flip
settings and install extensions without consent. Settings and add-on tabs
now never load web content, and the channels behind settings-preload check
their sender. `navigate` no longer accepts calls from web pages.

Add-on updates trusted any publisherSig, whatever name it carried, even for
bundled add-ons. The trust root is now the installed addon.json (publisher,
or the operator key when there is none); versions must be plain dotted
numbers; a community install can't take over a bundled or foreign id.

Also:
- autofill matches and fills against the live URL, not a stale prov.host
- bns:// forwards the raw request path (..%2F escaped the name's bucket)
- clipboard-read denied, openExternal asks; forged collision choices ignored
- web pages can't window.open file:/chrome:/theseus:; data:/blob: no longer
  go to the search engine; the quick-links panel loses home-preload
- clear-history-on-quit is awaited and removes history.json too
- update helper takes its paths from the environment (non-ASCII profiles)
- electrum poll has a deadline; misses wait at most 2.5 s
- p records go through Tor; add-on proxy credentials are actually used
- whole-folder require-cache bust on add-on version change; failed
  activate() no longer leaks its request filter
- approvals released when the window closes; web-app ids stay on-origin
2026-10-03 09:50:10 +02:00
Local Dev
2230d4200c feat(theseus): protections live in Settings › Performance; quiet dock for Shield and Cookie Pop-ups
Shield and Cookie Pop-ups are settings more than tools, so their
switches, the cookie mode, the counters and "Update rules" now sit in
Settings › Performance under a Protections heading, driven through the
add-ons' own message handlers (Settings-only IPC). Each card opens the
add-on's panel for the per-site details, and each panel links back to
Settings. The two add-ons start hidden from the toolbar's extension
row (manifest dock:"hidden", honoured once so a user who shows them
keeps them); "Show hidden" on the row brings them back.

theseus://settings and theseus://settings/<section> are now addresses,
so any page or note can link to a Settings page.

Also: a Settings or add-on tab that the user navigates elsewhere stops
counting as that tab, otherwise "open Settings" kept focusing a tab
that no longer showed Settings.
2026-09-27 20:37:30 +02:00
Local Dev
d4b9de93a6 feat(theseus): Cookie Pop-ups — consent banners answered automatically
A bundled add-on that answers cookie consent dialogs, rejecting all but
the essentials by default (or accepting, if the user prefers the banner
simply gone), so pages open without one. Built on DuckDuckGo's
autoconsent (MPL-2.0): its rule bundle covers hundreds of consent
managers, and a reject-button heuristic handles unknown banners in
reject mode. The library runs through the page-inject slot in every
http(s) frame's isolated world; the add-on hands each frame the user's
settings and the rules, and counts what was handled per site for the
panel, which also excludes a site with one click.

build-inject.js assembles inject.js from the library in node_modules
plus the glue, and copies the compact rules and licence into the add-on
so a rule update can ship through the add-on channel.

Host side: page-inject scripts get theseus.evalInPage for the few rules
that need the page's own JavaScript (they already reach the page via
contextBridge, so no new trust tier), and api.tabs is open to
page-inject add-ons as well as request-filter ones.
2026-09-27 19:55:43 +02:00
Local Dev
053d7bc127 feat(theseus): Shield — tracker and ad blocking as a bundled add-on
Theseus had no content blocking at all. Shield blocks requests to known
tracking and advertising hosts on every site, using EasyList and
EasyPrivacy through Ghostery's adblocker engine (the matcher those lists
are written for). The lists ship inside the add-on so blocking works
from the first launch, offline; the compiled engine is cached under the
add-on's data dir (a 22 ms load instead of a 500 ms parse), and the
lists refresh from their publishers about once a day.

The panel shows what was stopped on the current page, a one-click
allow for the site, the global switch, the running total and the rule
versions with an "Update now". Network filters only for now: a blocked
request never leaves the browser, but leftover empty ad boxes are not
hidden yet.

Host side: a "request-filter" capability. Chromium allows one
onBeforeRequest listener per session, so main owns it and consults the
add-ons' filters; a top-level navigation is never blocked, only http(s)
subresources are offered. api.tabs (active tab and a change event) lets
the panel show per-site numbers without seeing page content.
2026-09-27 19:43:14 +02:00
Local Dev
9d9c651b30 Merge branch 'claude/sleepy-maxwell-251ee6-b' into HEAD
# Conflicts:
#	TheseusNavigator/addons-host.js
#	TheseusNavigator/main.js
2026-09-23 00:25:36 +02:00
Local Dev
8174fccba0 feat(theseus+aegis): WizardConnect auto-detection — wiz:// links + page scan
Completes the three detection paths. The injected provider shipped in
0.8.8; these two needed host support, because nothing in the add-on API
could reach the active tab's content (captureTab is pixels, not DOM).

wiz:// links (main.js)
  A click on a wiz:// anchor is intercepted in will-navigate and in the
  window-open handler (target="_blank" lands there instead), and routed
  to the wallet with the offering page's origin attached, so the
  approval names the real site. The tab never navigates. This needs
  nothing from the dapp beyond rendering the URI as a link, so it works
  for third-party dapps that will never adopt a Silent Mode API.

scan-page capability (addons-host.js + main.js)
  New capability backing api.scanActiveTabForUris({scheme, limit}).
  Deliberately NOT a "read the page" API: the host runs the match and
  returns only the URIs found, so an add-on holding this still cannot
  see page text, markup or form values. It sits well below page-inject
  on the trust ladder — it learns that a page offers a wiz:// code and
  nothing else. Scheme is validated against [a-z][a-z0-9+.-]* and the
  result count is capped.

  The matcher also accepts WizardConnect's QR-alphanumeric spelling
  (WIZ://%3FP%3D…), which is frequently the only form present when a
  dapp renders its pairing code as a QR, and decodes it. Verified
  against the SDK: decodeKeyExchangeURI accepts standard, QR-raw and
  QR-decoded alike.

  Regex sources are built host-side and passed as JSON rather than
  assembled inside the injected string — hand-escaping backslashes and
  quotes through two levels of literal was both wrong on the first
  attempt and unreviewable.

Aegis
  Declares scan-page, adds the wcScanPage handler and a "Scan page"
  button next to Connect. A scan fills the URI field and stops there
  rather than pairing outright: the user still chooses which wallet
  signs and still presses Connect, because a scan that silently paired
  would carry far more consequence than the button implies. Older hosts
  without the capability get a clear "update Theseus" message instead of
  a dead button.
2026-09-23 00:16:49 +02:00
Local Dev
870986de21 fix(theseus): an add-on cannot restart the browser on its own
Aegis relaunches Theseus after staging its own update; seen twice in a
dev instance, the whole browser restarted with no warning, mid-session.
The add-on API's restartApp now asks the user in a native dialog
("Aegis Wallet wants to restart Theseus" — Restart now / Later, Later
is the default) and resolves { restarted, deferred }. Declining loses
nothing: a staged update applies on the next normal launch. The guard
lives in the host, so it covers every Aegis version on the channel and
any future add-on.
2026-09-22 21:59:44 +02:00
Local Dev
ed2d2e1d5a feat(theseus): plug-in dock under the Theseus menu
Aegis sat among the extension buttons and drifted as add-ons were
installed or removed. First-class Silent Mode components (manifest
category "plugin") now get a labelled chip at the right end of the
favorites row, directly below the Theseus menu — always visible, never
collapsed into the extensions overflow. The add-on host tags each
sidebar panel and toolbar menu with the category so the chrome can
split the two docks; the extensions dock keeps its behaviour.
2026-09-22 00:31:18 +02:00
Local Dev
bd6aab02fe feat(theseus): profile at %APPDATA%\Theseus, extensions under extensions\
The profile folder was Electron's default from the product name
("Theseus Navigator") and add-ons lived in addons\ under it. Now:

  %APPDATA%\Theseus\extensions\          installed extensions
  %APPDATA%\Theseus\extensions-data\     per-extension storage + scratch
  %APPDATA%\Theseus\extensions-backups\  replaced copies
  %APPDATA%\Theseus\extensions-staged\   staged updates

Both moves are one-time migrations on the first start that finds the old
layout: the profile folder is renamed (same volume, instant) or copied
when a rename is refused, with the old folder left in place in that case;
the four sub-folders are renamed before the extension host first reads
them. Nothing is deleted. THESEUS_USER_DATA still overrides everything.

The host now hands each extension its data folder as api.dataDir; the
Screenshot and PDF editor add-ons used to rebuild the old path from their
own folder for scratch files (so they recreated addons-data\ after the
move) and now use the field, with versions bumped so the bundles reseed.
2026-09-21 01:55:25 +02:00
Local Dev
decf118c88 feat(theseus/addons): context-menu-item capability + api.revealSidebar
Adds a new "context-menu-item" capability. Add-ons declare a
"context-menu-items" array in their manifest:

  {
    "capabilities": ["context-menu-item", ...],
    "context-menu-items": [
      { "id": "translate-selection", "label": "Translate selection",
        "when": "selectionText", "icon": "🌐" }
    ]
  }

The `when` filter is one of selectionText | linkURL | editable | image
| always. Right-click on a page, and items whose `when` matches the
current context get merged into the native menu after the built-in
Search-for entry, before Back/Forward/Reload. Both context-menu
handlers (main tab area + detached link windows) share the same
merging logic.

Picking an item dispatches "context-menu" to the add-on's onMessage
handler with the full context (selectionText, linkURL, mediaType,
srcURL, pageURL, host). The add-on decides what to do — the
translate add-on stashes the selection to storage and calls
api.revealSidebar("main") which surfaces its own sidebar panel.

api.revealSidebar(panelId) is the paired hook. Ownership is enforced
by the host — an add-on can only reveal panels it registered —
before routing to main's setSidebar path.

Unknown capabilities were already silently dropped by
validateManifest, so older Theseus builds that don't understand
"context-menu-item" just ignore it, and the manifest still loads.
Add-ons that also declare "sidebar-panel" keep working; the new
capability doesn't require it.

This is the wiring that pairs with the translate/ add-on landed in
4498fbb — right-click "Translate selection" is live once this ships.
2026-09-20 18:27:10 +02:00
Local Dev
f46e9112b7 chore(theseus): 0.3.47 — plug-in category + panel-driven addon self-update, aegis 0.6.31
Theseus core:
- addons-host: manifest.category ("plugin") propagates through snapshot(); new
  addon API surface checkAndStageSelfUpdate() + restartApp() so a plug-in
  can offer in-panel "update now → restart to apply" without pushing the
  user to Settings.
- main.js: wires the two new hooks into the AddonHost constructor.
- settings.html: Extensions listing filters out category==="plugin"; those
  add-ons live in Plug-ins instead, single source of truth.

Aegis 0.6.31:
- BTC picker trimmed to Signet only; testnet3 hidden (adapter kept so any
  existing wallet still loads).
- Wallet strip groups by chain, not chain:network; ticker gets a ▾ chevron
  and a dropdown listing every subnetwork with its own totals. Mainnet
  reads as the plain ticker; testnets carry a small Chipnet/Signet/Sepolia
  pill inline.
- Per-unit price sits directly under the ticker; amount + fiat mirror on
  the right — one glance covers name/price/holding/value.
- + Add and ⋯ More promoted from the strip into the header's action row,
  next to the new ✎ chip (was the redundant top ⋯). Duplicate "Manage
  current wallet" entry removed from the More menu.
- Footer update chip is a two-step flow via the new API: stage → restart.
  Falls back to opening Settings on any Theseus that lacks the hooks.
- Manifest declares "category": "plugin".
2026-09-14 02:30:51 +02:00
Local Dev
c9a3db26ce fix(theseus/boot): paint the toolbar first — stop gating startup on chrome.html's load event
Users saw a blank window with a white strip across the top for seconds
on launch. Root cause: every part of startup, including session restore,
waited for chrome.html's did-finish-load. That event also waits for the
page's subresources, and the bookmarks bar loads its favicons over
bns:// — a BNS lookup plus a network fetch each — so a slow link held the
whole boot. On top of that, seven hidden overlay renderers, every restored
tab, the BNS index build and three network fetches all started in the
same tick and stalled the main thread ~1 s while the toolbar tried to
paint.

- Continue boot at chrome.html's dom-ready (toolbar scripts have run, IPC
  listeners exist) instead of did-finish-load; 8 s fallback timer.
- Window and chrome view get the toolbar's --bg for the active theme so
  the pre-paint frame is never white.
- Overlay pages (site info, engine picker, downloads, suggestions,
  password fill, link status, approval) load 250 ms after the toolbar or
  on first use; the approval modal awaits its page so a dapp request
  can't hang.
- Session restore is staggered: active tab first, then one background
  tab per 150 ms slotted into its saved strip position. Session file v2
  records the active index; v1 arrays still load (active = last, as the
  old loop effectively did).
- AddonHost gains api.whenUiReady(); Aegis 0.6.2 defers its heavy
  dependency loading (noble precompute, bitcoinjs, libauth, WizardConnect)
  behind it.
- BNS snapshot warm-up still starts right after createWindow (bookmark
  favicons need it); Sia refresh, update check and home-card fetch move
  to the post-paint phase.

Measured on a clone of the real profile with nine restored tabs: toolbar
usable at ~0.7 s instead of ~1.5 s, main-thread stall during toolbar load
down from ~1.1 s to ~0.2 s.
2026-09-09 11:40:45 +02:00
Local Dev
992c02ea89 feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect
Aegis Wallet 0.4.4 → 0.6.1:

- Vault lifecycle from the wallet gate. The locked / not-yet-created states
  now show a master-password form (with optional BIP39 mnemonic on setup)
  instead of redirecting users to Settings › Passwords. New
  api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by
  the existing "vault-derive" capability. api.openSettings(section) also
  added; settings.html honours a #section hash on open.
- Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44
  path or a WIF; the cashaddr is derived in the add-on, the signer material
  goes to a separate wallet-imports.enc via api.vault.imports {list, add,
  remove, signer}. Argus password-vault gains createImports / unlockImports /
  saveImports with its own KDF salt so the imports key is disjoint from the
  passwords key. lib/chain-bch-imported.js is a single-address Electrum
  adapter; spend support is deferred to M.1b.
- Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted
  in add-on storage. Fiat lines under balances, in the wallet picker, and a
  portfolio total when 2+ wallets are open. Settings tab is now reachable
  while the vault is locked so the toggle is always available.
- WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js).
  @wizardconnect/{core,wallet} are loaded dynamically via api.import to stay
  on the right side of LGPL §4d. Sign requests go through approvalModal and
  are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS.
- DGB adapter load is now soft-fail: when Aegis runs from userData/addons the
  bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of
  taking the whole add-on down.
2026-09-09 10:33:21 +02:00
Local Dev
9d81c29656 fix(theseus/light): light-mode acid → BCH-teal #088A66 (brand-family, AA on white)
Prior light-mode --acid was #3a5c00 (dark olive-green) — legible but
off-brand. The Bitcoin Cash brand primary is #0AC18E (a teal-leaning
green already used in bchwallet's --bch variable). Darken it a step to
#088A66 for AA text contrast on white (~5:1) while staying in the BCH
family — the light-mode accent now reads as "Bitcoin Cash green,
darkened for legibility" instead of an arbitrary olive.

Applied across every chrome page + addon panel that carries the light
override (chrome / settings / error / home / approval / messages /
bchwallet / siawallet / screenshot editor). Dark mode's #d6ff3d
untouched.
2026-09-08 00:49:46 +02:00
Local Dev
118de0ef5c feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit)
Aegis now covers four coins across two-step coin+network picks: BCH
(mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet).

- Sia (SC): pulled the standalone siawallet's lib into
  bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing
  the common adapter shape. The very first SC wallet the user adds in
  Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry
  over automatically; subsequent SC sub-accounts start at
  "bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL
  shows a "Point Aegis at a walletd node" gate in the panel.
- Vault-derive gate now honors a manifest-declared `absorbs` list, so
  Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive()
  guard accepts paths under either the current id or the absorbed one —
  the mechanism a superseding add-on uses to inherit an older add-on's
  keyspace without orphaning funds.
- DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the
  SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit
  (m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32.
  ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool).
  BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no
  FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked
  against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address
  for "abandon×11 about, m/84'/20'/0'/0/0" is
  dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39.
- Panel: SVG coin logos for SC (green disc with S) and DGB (blue
  octagon with D) alongside the BCH/TRX marks. Chain-specific settings
  block per coin (walletd URL for SC; derivation path for DGB). Balance
  render uses BigInt-safe arithmetic so 24-decimal SC amounts don't
  lose precision on the way through the panel; amount input on SC
  returns a hastings string.
- Every chain adapter's snapshot fits the panel's shared shape
  (address/balance/history/etc.), so future chains only need a new
  chain-<x>.js file, a COINS registry entry, a matching case in
  mountWallet, and an SVG logo.

Standalone siawallet addon stays as-is on disk; users can delete it once
they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
Local Dev
15694195d6 feat(theseus/screenshot): full-tab editor + toolbar-menu + open-tab capabilities
Reworks the screenshot addon into the flow the user asked for: the
dock icon opens a small dropdown menu (Visible viewport / Full page /
Region…) instead of the sidebar picker, and each capture opens a
full browser tab hosting an editor.

Two new addon-host capabilities land alongside:
- toolbar-menu: the addon declares an icon + item list in its manifest;
  the chrome dock renders a button that, on click, opens a small menu
  and dispatches the selection to the addon via addon-menu-select IPC.
- open-tab: api.openTab(path) opens a browser tab whose URL is the
  addon's local file. Origin-gated per addon; the editor uses a
  dedicated addon-tab-preload for its main → renderer bridge.

Editor page (editor.html/js/css):
- Crop, arrow, rectangle, circle, freehand pen, text, blur
- Colour swatches (red / yellow / acid / white / black), 3 stroke widths
- Undo/redo command stack, zoom controls
- Save PNG (goes through the download pipeline, chip picks it up)
- Copy to clipboard via ClipboardItem
2026-09-07 00:56:57 +02:00
Local Dev
57d71a0996 Ship Theseus 0.3.13 7d88e4c4 (Ariadne toggle + 'p' record + Screenshot addon)
Setup    7d88e4c46b02448e40d6075d10f2c6688c6d60c5a9ec41b9cbcb7684f131d6e1
Portable 5a4bcc6abb21c23729d79dd600142df4f171cc3d6bf71716dae1c802ac10e48f

Bundled since 0.3.12:

1514793 - Settings > Registries gets an on/off toggle for Ariadne's
Thread (system-wide BCDN resolver for non-Theseus browsers). Query is
silent Get-ScheduledTask; toggle spawns elevated PowerShell (UAC once
per action). Three states: running / stopped / not-installed.

b16f0a1 - New BCDN 'p' record type in Argus record-picker + Theseus
serving. Reverse-proxies an upstream URL under a BCDN name, keeping
the BCDN name in the address bar; uses upstream's own DNS + public
CA + Host header (unlike 'ip' which pins IP + on-chain TLS fingerprint).
Placed after 'ip' in the apex chain, suppressed under subdomain
inheritance so a 'p' name doesn't silently proxy every subdomain.

cfec253 - Argus registrar gains buildTldRegistrationTx + TLD_BEACON +
normalizeTld exports for minting per-TLD certificates per the TLD-
registry design.

bbfc05c - Bundled Screenshot add-on: capture-tab capability + sidebar
launcher for visible / full page / region modes; saves to Downloads.
Follow-up task_b9608dc6 will rework this into a full-tab editor.

Deployed. Verified LIVE 0.3.13.
2026-09-07 00:36:08 +02:00
Local Dev
40391798e0 feat(theseus/bchwallet): per-site payment allowance for remembered sends
The dapp send approval gains an "Afterwards" dropdown: ask every time, or
allow up to 0.001 / 0.01 / 0.1 BCH more without asking. The allowance is
stored as permissions[origin].sendTx {capSats, usedSats}; sends within the
remainder go through silently and draw it down, a larger request re-prompts
(showing what is left) and the choice made there replaces the allowance.
No unlimited option. Settings > Connected sites shows the remaining budget
and Revoke clears it. Message signing still asks every time.

Host: approvalModal accepts `select` {id, label, options}; a chosen value
comes back as "+<id>=<value>" and is validated against the offered options.
2026-09-06 12:43:17 +02:00
Local Dev
de576935c1 feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
  (@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
  lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
  recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
  digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
  client with failover + subscriptions), lib/wallet.js (gap-limit scan,
  balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
  against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
  History (deltas, confirmations, explorer links), Settings (derivation
  path, electrum server list, xpub / approval-gated xprv reveal). Locked
  and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
  add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
Local Dev
ffeda26345 feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:

- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
  vault is unlocked with a 32-byte HKDF child of the vault root under
  "silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
  preload asks main (sync, against the committed URL) which add-on bridges
  apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
  checkbox}) shows a consent overlay over the tab area (approval.html);
  resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
  messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
Local Dev
ee0548fec9 Ship Theseus 0.2.2 972f6209 (Extensions rename + draggable sidebar + session-proxy)
Setup    972f6209639122f32f032d5f2f9fc5a4808e0d4a810f88ae38ec9a1275ac52ac
Portable a9771f054ec36b9aff6ddee958cecf7e84cdacd4d7932aa8385c445aab4d29be

User-visible rename: the Settings tab and its labels say "Extensions"
now instead of "Add-ons". Internal identifiers (disabledAddons, the
addons/ folder, IPC channels, capability strings) stay put — code
churn wasn't worth it, and users only see the user-facing text.

Draggable sidebar. sidebar-preload.js now injects a 5px grip strip
along the LEFT edge of every panel document. mousedown+mousemove
streams delta-x px to main via sidebar-drag IPC; main clamps to
[200, 800] and debounces a save to settings.sidebarWidth. Width is
restored on next launch. The default is still 340. Faint acid-green
highlight on hover so the affordance is discoverable.

New extension capability: session-proxy. An extension whose addon.json
declares "session-proxy" gets api.setSessionProxy(rules) which routes
to session.defaultSession.setProxy — the same primitive Tor already
uses under the hood. Rules can be a string ("socks5://host:port") or
an object matching Electron's setProxy shape; null clears. The
capability is opt-in: an extension without the declaration gets an
error if it tries to call setSessionProxy. This is the framework
surface a private 3-VPS relay extension would build on (extension
folder stays on the operator's disk only; nothing about it appears in
the public build).

Deployed: scp installers + manifest + tools/ + releases/ pages to
VPS, sia-upload of both trees, verified HEAD 200 and manifest 0.2.2.
2026-08-31 16:00:34 +02:00
Local Dev
0117986657 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