theseus/GOTCHAS.md
Local Dev 434f6d35d0 Theseus: hand-off for moving Aegis to on-demand activation
Aegis is most of the remaining launch cost: ~165 ms of synchronous
activation, then ~610 ms of main-thread work loading its crypto and
WizardConnect deps. It was left on startup because another session
owns bundled-addons/aegis. NOTE-aegis-on-demand.md says exactly what it
needs, measured against 0.27.1:

- addon.json: "activation": "on-demand", plus panels [{id:"main",
  title:"Wallet", page:"panel.html"}].
- activate() must return a promise that resolves once deps are loaded
  and, only if the vault is already unlocked, wallets are mounted.
  vault.derive never rejects on a locked vault, so awaiting
  mountAllWallets there would stall every first call for the full 5 s
  host wait.
- Persisted WizardConnect pairings (wc/<walletId>/uris) need a relay
  listener: call api.startAtLaunch(true) while any exist, false when
  none are left.
- Bridge calls, panel open, wiz:// links and Settings calls already wake
  it. The dapp globals are injected from the manifest before it runs.

GOTCHAS: an enabled add-on may not be running, so code in main reaches
add-ons through dispatch() rather than checking isActive()/hasHandler().
2026-10-03 16:05:41 +02:00

7.1 KiB
Raw Blame History

Theseus gotchas

Non-obvious things sessions keep re-deriving. If you found this file because something broke in a way that felt spooky, the answer is probably here.

build.files in package.json is EXPLICIT

electron-builder ships only what's listed in build.files. Every runtime- loaded HTML/preload/module MUST be there. A missing entry causes the loadFile("foo.html") in a WebContentsView to silently open blank in the packaged app — visually indistinguishable from "the button does nothing".

npm start (dev) never catches this because it reads from the source tree directly. Only a real npm run dist + install exposes it.

Every time you add a new WebContentsView or loadFile, add its HTML and preload to the files array. Sanity check after building:

node -e "const b = require('fs').readFileSync('dist-public/win-unpacked/resources/app.asar'); for (const n of ['engine-picker.html','popover.html','downloads.html','engine-picker-preload.js','popover-preload.js','downloads-preload.js']) console.log(n, b.indexOf(Buffer.from(n)) >= 0 ? 'OK' : 'MISSING');"

This has bit us once (search-picker dropdown, shipped 6bbccf9c, blank picker). Do not let it bite twice.

Building — admin terminal the first time

npm run dist needs an admin terminal on the machine's first Theseus build so electron-builder's winCodeSign package can extract its darwin/ symlinks (requires SeCreateSymbolicLink). Subsequent builds reuse the cache under $LOCALAPPDATA/electron-builder/Cache/winCodeSign/ and don't need admin.

If DNS is misbehaving on the machine, pin these hosts in C:\Windows\System32\drivers\etc\hosts before building so electron-builder can fetch: github.com, codeload.github.com, objects.githubusercontent.com, release-assets.githubusercontent.com. Strip them after.

Reproducible builds

Set both before npm run dist:

  • SOURCE_DATE_EPOCH — freezes timestamps so a rebuild with no content changes produces the same hash. Bump per release, not per rebuild.
  • CSC_IDENTITY_AUTO_DISCOVERY=false — stops electron-builder searching for a signing cert (we don't sign; SECURITY.md rule 2).

Native <select> popup theme

Chromium's native <select> popup uses the page's color-scheme. When it's "light dark" (both accepted), Chromium picks by the OS scheme — so a dark-themed app on a light OS shows a light popup and vice versa.

Fix, currently in settings.html:

:root { color-scheme: dark; }
@media (prefers-color-scheme: light) { :root { color-scheme: light; } }

nativeTheme.themeSource (driven by settings.theme) sets prefers-color-scheme, so this stays in sync automatically. Do not remove.

Chromium also respects select option { background; color } in the popup on Windows — a belt-and-braces override alongside color-scheme.

A preload belongs to the WebContents, not the page

A view's preload runs for every document that view ever loads. Navigating a Settings tab to a website used to hand that site window.cfg, which covers the vault (pwGet), settings and extension installs. Two rules follow:

  • Settings and add-on-file tabs never load anything else. navigateTab and will-navigate open the target in a fresh tab instead.
  • A new IPC channel exposed through settings-preload.js must be added to SETTINGS_ONLY (or SETTINGS_SHARED if the toolbar's preload.js calls it too) in main.js. The wrapper around ipcMain.handle only checks the sender for the channels in those sets. Any other channel is callable by whatever page ends up in the view.

The same applies to home-preload.js, which is in every tab. Handlers behind it check isHomePageSender / isErrorPageSender, which compare the sender against the exact shipped file:// URL.

An enabled add-on is not necessarily running

An add-on whose addon.json says "activation": "on-demand" is listed at launch but not started. Its declared panels, toolbar-menu, context-menu-items and page-inject bridge are live anyway, and AddonHost.dispatch() starts it on the first call (see ensureActive in addons-host.js). So in main:

  • Reach an add-on through addonHost.dispatch(...), never by checking isActive() / hasHandler() first. Both are false for a waiting add-on, and the call would never wake it (routeWizUri shows the pattern for when you really need to check).
  • "Enabled" in the snapshot means running or waiting; running tells them apart.
  • Settings › Performance › "Start extensions when first used" off makes every add-on start at launch again, which is the quickest way to rule this out when an add-on "does nothing".

The resolver in Theseus is resolver-web.mjs, not .js

Packaged builds ship Argus/src/lib/resolver-web.js as resolver-web.mjs (see extraResources in package.json). Renamed because resources/ has no adjacent package.json so a .js gets treated as CommonJS and its exports fail. Dev reads the engine copy directly (Argus is type: module so .js works there).

The resolver's electrum WS directIP fallback

main.js passes directIP: true to resolveHost so the engine can dial the chipnet electrum servers by pinned IP if system DNS is dead. Do not remove — this is what keeps .bch resolution working when the adapter DNS is broken (a real failure mode we've hit).

Publishing new hashes end-to-end

  1. Build (see above).
  2. Update ../site/releases-manifest.json AND ../site/tools/index.html AND ../site/releases/index.html with the fresh SHA-256s + release date. All three must match.
  3. Deploy:
    scp dist-public/*.exe ../site/releases-manifest.json silentmode:/opt/silent-mode/dl/
    cd ../Argus && node src/lib/sia-upload.js ../site bns/silentmode
    
  4. On-chain (wallet spend, user's action): releases.silentmode.bch currently publishes {"u":"https://dl.silentmode.st/releases-manifest.json"} — a compact u record because on-chain hashes wouldn't fit the 200-byte OP_RETURN. Only re-publish if the URL changes or if we ever put full hashes on-chain.

Sia upload from this machine fails without a hosts pin

node src/lib/sia-upload.js fails with getaddrinfo EAI_FAIL coinspectrum.duckdns.org unless the host is pinned in C:\Windows\System32\drivers\etc\hosts: 195.184.247.106 coinspectrum.duckdns.org. This is a DNS quirk of the machine, not a code bug — related to the strict-resolver behavior the Ariadne daemon's EDNS fix addresses.

Deploy directory quirks

  • /opt/silent-mode/dl/ is served as dl.silentmode.st — installers + manifest live here.
  • silentmode.st proxies to Sia (bns/silentmode/) — the HTML pages live there.
  • The bns gateway does NOT auto-index directories. silentmode.st/apps/ is a 404 (NoSuchKey); users must hit /apps/index.html. Fix upstream in bnsd.js if it ever matters.

Firefox needs a restart after Ariadne CA install/rotation

Firefox only reads root CAs at startup. The Ariadne installer offers a -CloseFirefox task by default when Firefox is running so this doesn't bite. Chrome/Edge pick up the new root immediately.