PIN checked by the host with a hard five-guess limit, permits described
from what is signed, unreadable WizardConnect requests refused, PIN
clearances bound to their request, permissions re-read after the PIN
wait, and the Tron duplicate-field fix (also shipped alone as 0.30.1).
- send, sendToken and consolidate now require the wallet id the panel
reviewed them for; a missing id used to skip the check, and the Solana
token send sent none, so a selection change under the PIN pad sent from
another wallet.
- "1 0" was read as 10: a space inside an amount is now refused.
- A BCMR registry list saved before https was enforced is filtered on read,
and names lose the soft hyphen, Arabic letter mark, Mongolian vowel
separator, line/paragraph separators and Unicode tag characters too.
signAndSend took a permissions snapshot, waited up to two minutes for the
PIN, then wrote the snapshot back. Revoking the site meanwhile still let
the allowance payment go out and restored the revoked allowance, and any
other permission change made during the wait was lost. After the wait it
now re-reads permissions, refuses an allowance payment whose allowance was
revoked, replaced or no longer covers it, and changes only this origin's
sendTx.
One global 90 s clearance was opened by every PIN proof. Opening the
wallet or ticking a setting let the next dapp transaction from any site
through without a PIN; a dapp waiting in its poll could take the clearance
the user had just made for their own send; and with two sites waiting the
second one's request was dropped.
Each waiting dapp transaction now has an id, and only a proof naming that
id releases it; a proof given for "transaction" in the panel clears the
panel's next send only; any other proof clears nothing. The panel answers
waiting sites one at a time. Promote to HD now asks for the PIN like any
other spend instead of failing when PIN-per-transaction is on.
wc-sign accepted a flat request (tx and sourceOutputs at the top) that
buildWcApproval does not read, so any paired dapp could get a signature
after an overlay showing only the wallet, an input count and the sighash.
The signer now takes only the nested WizardConnect shape, and the overlay
refuses, without showing anything, a request whose outputs or spent
inputs it cannot decode. Total out now sums every output, not the first 8.
describePermit read td.message directly, and the EIP-712 encoder ignores
keys a type does not declare. A dapp could put a decoy allowed:false or
details:{amount:"1"} beside an unlimited permit and the overlay showed
the decoy, not risky, under the plain Sign button, with the same digest.
The overlay and the message preview are now built from the declared
fields only, the permit variant is picked from the declared type, and
dropped fields are counted on the overlay. Amounts of 2^96 units and up
are flagged as effectively unlimited, marketplace orders and Safe
transactions get the warning too, and the encoder refuses a non-hex
address instead of signing it as zero bytes.
The panel fetched the PIN blob and decrypted it itself, then reported its
own failures. The lockout therefore counted only what a well-behaved panel
chose to report, a 15-minute timer handed out five more guesses forever,
and anything able to run in the panel could take the blob and search the
million PINs offline in minutes.
The blob now never leaves index.js: pinSet builds it after checking the
master password against the vault, pinUnwrap counts each guess before
trying it, and five wrong guesses switch the PIN off until the master
password is entered. The panel keeps its PIN pads and only sends digits.
A blob from an older build (200k iterations) is re-made at 600k under a
fresh salt on the next correct PIN. Only the topmost PIN pad listens to
typed digits, so two stacked pads cannot both take one entry.
The decoder read the first copy of a singular protobuf field; java-tron
keeps the last. Any.value is opaque bytes, so a TransferContract carrying
two recipients and two amounts hashes to the same txid either way: the
overlay showed "1 TRX to X" while the chain would move 999 TRX to
another address. It also defeated the plan-time and sign-time draft checks
against a hostile node. A repeated singular field, or a known field with
the wrong wire type, now refuses the transaction.
Still 0.31.0 (unpublished batch).
- ETH nonce. The pending count from a load-balanced RPC often misses a
transaction this wallet sent seconds ago, so two sends in a row shared a
nonce and the second failed or replaced the first. The nonce is taken at
signing, from the RPC or from what the wallet itself last broadcast,
whichever is higher, and broadcasts are serialised per wallet.
- Chains without EIP-1559 (no baseFeePerGas) get a legacy EIP-155
transaction; they rejected the type-2 envelope, so a network added by a
dapp could receive but never send. The tip is clamped to the fee cap.
- Bitcoin and DigiByte take their fee rate from the Electrum server's
estimate instead of a constant, size each output from its real script
(a taproot destination was undercounted), and round the size up before
pricing. Bitcoin inputs signal replace-by-fee. DigiByte keeps its 20
sat/vB floor and does not signal RBF, which it does not have.
- Electrum: a wallet with live subscriptions went quiet for good when its
server dropped. The client reconnects with backoff, pings to catch dead
sockets, times out a silent connect, and hands the replayed subscription
answers on as notifications so the wallet refreshes. dispose() ends it.
- Max with an SPL token selected did nothing; it now fills the exact token
balance.
- Removing a wallet retired its derivation index for good. Add wallet now
takes the lowest free index, so the same wallet comes back.
- Solana: registered through the Wallet Standard as "Aegis" instead of
setting isPhantom, with silent connect for already-connected sites.
- "Only show the wallet to sites I enable": Aegis keeps the host's
page-inject allow-list in step with enabled and connected sites.
PIN
- The PIN blob wraps the vault master password under six digits and sat in
plain add-on storage, so a copy of the profile reduced the master password
to a million offline PBKDF2 guesses. It is sealed with the OS keystore
before it is stored; a plain blob from an older build is sealed on first
read. New blobs use 600k iterations.
- "Ask for PIN on every transaction" was decided by the host and enforced
by nobody: `send` never checked it and dapp transactions had no PIN step.
A gate is now cleared only by the master password the PIN unwraps,
verified against the vault, which also opens a single-use transaction
clearance. Panel sends consume one; dapp transactions ask the open panel
and wait.
Spending and signing
- Consolidate emptied wallets on the panel's confirmation alone, defaulted
to every sibling when no list was sent, and swept into whatever was
selected at click time. It now needs explicit sources and the previewed
destination, and shows the whole batch on the host overlay.
- Typed data for a chain other than the connected one is refused. Permit
and Permit2 signatures name the spender, tokens, amounts and expiry, and
an unlimited one gets the danger action. Previews are no longer cut at
600/400 characters without saying so.
- eth.rpc relayed any eth_* call for sites that never connected.
- The WizardConnect overlay lists the tokens being spent and received.
Send form
- "0,5" was read as 5: the parser deleted commas. Amounts are parsed
exactly; a decimal comma is a decimal, ambiguous or non-numeric input is
refused, extra decimals are an error instead of being dropped.
- Send submits the request the summary was computed for, never a fresh read
of the form, and stays off while a plan is pending or stale.
- Switching wallet resets the form instead of leaving a live button on the
previous wallet's plan.
- The unlock field kept the master password after unlocking; the PIN pad
kept its digits and kept counting keystrokes typed elsewhere as PIN
attempts; an idle lock left a revealed key or seed form on screen.
- Enter confirmed a dialog even with focus on Cancel.
Start of the next batch; 0.30.0 is published.
- Tron panel sends signed whatever /wallet/createtransaction returned while
the approval showed the local request. The returned bytes are now decoded
and must be one transfer from this wallet, to that address, for that
amount, with a matching txID - checked at plan time and again at signing.
- Importing a Solana wallet from a seed phrase threw on every attempt (a
mis-parenthesised `new require("crypto").createHmac` plus a bare require
of an ESM-only subpath). SLIP-0010 now uses Node's HMAC, as chain-sol does.
- Imported BCH wallets put token-bearing UTXOs into coin selection. They are
excluded, as in the HD wallet, and the balance counts what can be spent.
- WizardConnect dropped the token from each spent output before signing, so
under SIGHASH_UTXOS every signature of a token transaction was invalid.
- A wallet disposed while a refresh was in flight re-armed its poll timer.
- BCMR registry content is bounded before it reaches the panel: control and
bidi characters stripped, lengths capped, decimals 0-18, icons https/ipfs
only, registries https only.
Aegis is getting a standalone forge repo (silentmode/aegis) split from
this folder. Inside Theseus it was covered by the root LICENSE; on its
own it needs the license and a description in the folder itself.
WalletKeys.entry() built a bech32 p2wpkh address whatever the account
path's purpose, so picking Legacy (D...), Wrapped SegWit (S...) or
Taproot (dgb1p...) in Settings showed a dgb1q address from the BIP44/
49/86 key tree, one no other wallet restoring that path would find.
Each family now derives its own address and script, and spending
supplies what its inputs need (previous tx for P2PKH, redeem script for
P2SH-P2WPKH, tap-tweaked key for Taproot, which is active on DigiByte),
with per-family fee sizes. Unknown purposes are refused instead of
falling back to BIP84.
Also: inputs were signed in selection order but the PSBT orders them by
BIP69, so a spend from two addresses tried to sign each input with the
other's key. They are now signed in the PSBT's order.
approvalRequest passed approve/reject keys the host overlay does not
know, so it showed a lone "OK" button whose id never equalled
"approve": every WizardConnect signing request was refused, and the
HTML body was shown as literal markup. It now passes a Sign action and
plain rows: wallet, input count, each output decoded to a cashaddr on
the wallet's network (or OP_RETURN / raw script), total, and who
broadcasts.
lib/dgb/deps.js is ESM, but in a dev checkout the nearest package.json
is TheseusNavigator's, which says "type": "commonjs"; the import threw
and DigiByte was silently unavailable whenever Theseus ran from the
repo. Shipped installs have no package.json above the add-on, so they
were unaffected. lib/dgb/core and lib/dgb/psbt already carry the same
marker.
The isolated-world relay accepted any postMessage carrying the fixed
tag "aegis-aegis", including one from a cross-origin iframe (an ad,
an embed), and attributed it to the top-level origin and its grants.
The tag now carries a per-load random nonce that only the injected
main-world bridge knows, and both listeners drop events whose source
is not this window. The document_start install path is unchanged.
The overlay for tronWeb-built transactions was built from the dapp's
raw_data JSON, which need not match raw_data_hex: a site could show
"1 TRX to X" and get a signature over anything. lib/tron-decode.js
decodes Transaction.raw from raw_data_hex (contract type, owner, to,
amount, TRC-20 transfer/approve calldata, fee limit, memo); the txID
must match the bytes, every contract's owner must be this wallet, and
unlimited approvals or permission/resource delegation get a danger
action.
sendRawTransaction relayed any signed transaction a site handed it;
it now broadcasts only txids Aegis itself signed.
- signAndSendTransaction signed String(Uint8Array) ("1,2,3,..."), so
every dapp transaction got an invalid signature. Adapters gain
signBytes(), which signs the exact message bytes.
- v0 (VersionedTransaction) messages were parsed with the version byte
as the header; the shared parser handles legacy and v0.
- window.solana.signTransaction went through signMessage and its
"moves no SOL" overlay. It now has its own handler and overlay, and
signMessage refuses bytes that parse as a transaction (Phantom's rule),
since such a signature is a valid transaction signature.
- Overlays decode System transfers and SPL transfer / approve /
set-authority, and list everything else as not decoded.
- eth_sendTransaction dropped the calldata, gas and fee fields, so an
ERC-20 transfer went out as a 0-value send to the token contract and
any contract call was broadcast as something else. plan() now carries
data/gas/fee caps/nonce, estimates gas for calls, and the overlay
decodes transfer/approve/permit/setApprovalForAll, flags unlimited
approvals and calldata to a non-contract, and shows estimated vs max
fee.
- personal_sign signed the hex string ethers/viem send as literal text;
it now signs the decoded bytes.
- wallet_switchEthereumChain flipped the global selected wallet, so any
site could move every connected dapp to another chain. Chain is now
per origin; the sidebar selection no longer redirects a dapp.
- wallet_addEthereumChain silently persisted a connection for chains
Aegis already had, handing the address to any site. Adding a chain
no longer grants anything, and RPC URLs must be https.
- eth_sign (blind hash signing) is disabled, as in MetaMask.
Connecting without ticking "Always allow" stored nothing, so the
site's very next call (personal_sign, signTransaction, ...) failed with
"not connected". Plain Connect now grants the origin in memory until
Theseus restarts; "Always allow" still persists. Every bridge (BCH,
Tron, EVM, Solana) checks grants the same way, revoke clears both, and
the connected-sites list shows EVM/Solana grants and which ones are
session-only.
The 2026-09-13 audit of the dapp bridges found real signing bugs whose
fixes were never committed; 0.30.0 carries them, ported onto the current
add-on.
Aegis was most of what was left of launch cost: its crypto and
WizardConnect deps block the main thread for ~0.6 s right after the first
frame. It now declares its panel and "activation": "on-demand"; the
dapp bridges are still on every page from the start, and the first page
call, panel open or wiz:// link starts it.
activate() returns a promise the host waits on, so the call that woke
Aegis finds WizardConnect and the mounted wallets. With a locked vault it
does not wait for the mount (derive blocks until unlock), so the page gets
the usual locked answer in ~0.7 s instead of after the host's 5 s limit.
Two things only work while Aegis runs: a live WizardConnect pairing (no one
else listens on its relays) and "stay unlocked" (which also opens
Settings > Passwords). While either is on, Aegis asks the host to start it
at launch, and withdraws the request when both are off. Pairings left
behind by a removed wallet do not count.
Boot trace, same profile, 3 warm runs: main thread blocked in the first
6 s 970-1040 ms -> 300-320 ms, longest block 605-667 ms -> 227-244 ms,
toolbar 1.34-1.61 s -> 0.83-0.87 s.
The main-world bridge is appended at document_start, which often runs
before the page has an <html> element. The append threw on null, and the
catch marked the origin as Trusted-Types-blocked in localStorage, so every
later visit skipped window.ethereum, window.solana and the EIP-6963
announcement on that site. On example.com it failed on 6 of 6 loads.
Wait for <html> with a MutationObserver (it fires at the microtask
checkpoint before the first parser-inserted script, so the bridge is still
first), remember only real Trusted Types refusals, and use a new key so the
origins wrongly marked by the old one get the bridge back.
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.
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".
Rebuilt from Pithos/ with scripts/build-theseus-addon.mjs. Pithos moves from a toolbar menu that opened a loopback tab to a left-sidebar panel. Existing installs get the same version over the extension update channel.
Ships Pithos (the s3d control panel) as a built-in extension: the dock
menu opens it in a tab served from a loopback port, and "Stop s3d"
shuts the gateway down. Generated by Pithos/scripts/build-theseus-addon.mjs.
yaml is vendored under vendor/yaml/lib rather than node_modules/ or
dist/, because Theseus git-ignores both and a clean-worktree release
build would otherwise ship the add-on without its only dependency.
panel.html loaded lib/jsqr.js synchronously: 257 KB and 10,105 lines of
vendored decoder parsed on every panel open, so on every hide/show, for a
feature most sessions never touch. It is now fetched the first time someone
imports a QR image, with concurrent callers sharing one load and a failed
load retryable rather than cached as a permanent rejection.
qr.js and panel.js get `defer`, so the browser can paint the (already dark)
document before executing 300 KB of panel.js. Order is preserved, so qr.js
is still defined before panel.js runs.
Parsed on open: 650 KB -> 399 KB.
This shortens the blank window but does not remove it. The white box itself
is Theseus-side: the sidebar panel's view is built as
new WebContentsView({ webPreferences: { preload: "sidebar-preload.js" } })
with no backgroundColor, and Electron defaults a view's background to white —
so white shows from view-attach until the page paints. Every other view in
the app passes a colour, and registerSidebarPanel takes only
{id, title, icon, page}, so an add-on cannot declare one. Not fixed here:
main.js belongs to the Theseus work in flight.
Second half of the Theseus-lag investigation. 0.27.1 removed the 7.4 MB
store; this removes the two things that produced it and the latency that
came with it.
Measured on this profile's own cache: 6,981 transactions, 7.38 MB, and one
consolidation with 400 inputs (median 2).
- loadHistory() awaited getTx() per displayed transaction and then again per
INPUT, strictly one at a time. The 400-input row alone cost 400 serial
round trips. Both waves are now prefetched with bounded parallelism
(getTxMany, 12 at a time). Against a simulated 10 ms link the same 473
fetches take 771 ms instead of ~4,730 ms; on a real 30-50 ms link the
serial version was 15-25 seconds per refresh, per wallet.
- Parent transactions were persisted forever. They exist only to compute a
delta for the 25 rows on screen, and keeping every one ever seen is what
grew the file. Only the displayed window is written now — 25 entries,
16.4 KB in the harness — while parents stay in a process-lifetime map
bounded at 20,000, seeded from the window so a restart does not refetch
what is already visible. A second refresh issues zero fetches.
TX_CACHE_VERSION 4 discards v3 caches. The v3 cap of 400 was also exactly
the wrong number for this data: a 400-input transaction needs 401 entries,
so it would have evicted and refetched on every single refresh.
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).
Ported from Digibyte.X/dgb-wallet @ 989a2696.
BIP32 builds the master key as HMAC-SHA512("Bitcoin seed", seed). DigiByte's
official 2018-19 Android/iOS wallets were BreadWallet forks and used
"DigiByte seed" as the HMAC key instead, so the same twelve words produce a
completely different key tree — every address differs, and a standard scan
finds nothing at all. Anyone importing one of those seeds into Aegis got a
valid, empty address and a zero balance with no way to tell why. Same shape
as the Bitcoin.com coin-type-0 problem.
- rootFromSeed() in the vendored lib/dgb/core/hd.js takes an optional HMAC
key, matching upstream, and exports HMAC_BITCOIN_SEED /
HMAC_DIGIBYTE_SEED.
- lib/import-derive.js grows the same option, since that is what actually
derives the address on an import, and importWallet records the variant on
the spec so the entry says which tree its stored address came from.
- The path scanner added in 0.24.0 now covers DigiByte: all four purposes
under the standard key, plus m/0' and BIP44 under the legacy one. So the
answer to "which tree holds my coins" is a scan rather than a guess, and
the Use button carries the key along with the path.
Verified against an independent HMAC-SHA512 computation, and end to end: the
same mnemonic gives DG1Khh…N1i under the standard key and DMWQ1g…PHse under
the DigiByte key, both valid, with m/0' deriving DGAf4M…Wyn.
Also fixes a coupling this exposed: lib/import-derive.js derives taproot
addresses but never called initEccLib, relying on chain-btc.js (and formerly
lib/dgb/core/address.js, before it went lazy here) doing it at load time. It
now installs the schnorr backend itself, once, so its taproot output no
longer depends on an unrelated module's import order.
The gap-limit change in 0a5d7ade (scan gap 200/100 for DigiScope parity) is
upstream-only for now — Aegis's imported DGB adapter watches a single stored
address rather than scanning a gap.
Reported as "the Aegis DigiByte import error". The vendored @dgb-wallet/core
modules under lib/dgb/ imported "bitcoinjs-lib", "bip32", "ecpair",
"bip39" and "@bitcoinerlab/secp256k1" as bare specifiers. Node's ESM resolver
walks up from the importing file, so that resolves in a dev checkout — where
Theseus's node_modules sits above the add-on — and resolves nowhere once
Aegis is running from <userData>/extensions/aegis/, which is every OTA
install. The import threw, index.js caught it and set dgbCore = null, and any
DigiByte import then died in digibyteNetwork() with "DGB adapter not
available".
So DigiByte worked on a fresh install and disappeared after the first update.
Confirmed on this machine: the only node_modules reachable from the installed
extension carries bip39 and none of the other four.
lib/dgb/deps.js now holds the dependencies, injected by index.js from
api.require (which resolves against the app tree) before anything under
lib/dgb/ is imported — the same pattern every other lib/*.js in Aegis already
uses, and the reason that pattern exists. Consumers read them through
accessors rather than capturing them at module scope, so import order is no
longer load-bearing: hd.js builds its bip32 on first use and the two
initEccLib callers go through a once-only ensureEcc(). Using a DGB module
without injection now throws a named error instead of a resolver failure
swallowed into a null.
Verified by loading the modules from a directory with no reachable
node_modules and deriving real addresses: BIP44 D…, BIP49 S… with its legacy
3… pair, BIP84 dgb1q…, BIP86 dgb1p… taproot, plus a WIF round-trip and the
non-DGB WIF rejection.
Reported as "opening Aegis makes Theseus get stuck / not responding", and it
was Aegis's fault.
The host's add-on store is ONE JSON file per add-on, and storage.get() does a
readFileSync plus a JSON.parse of the whole thing on every call —
synchronously, on the Electron main thread, the thread that drives the entire
browser. storage.set() additionally stringifies and writes all of it.
That store had grown to 7.4 MB, 99.9% of it one wallet's txCache: getTx()
kept every transaction it ever fetched, with full vin/vout arrays, and a busy
chipnet test wallet had thousands. Measured on the real file: parse 59 ms,
stringify 39 ms. So one storage.get blocked the UI for ~60 ms, one set for
~99 ms, and fullState() — which reads the store seven times over
selectedWalletId, walletRoles, walletEntries, snapshotForSelected and the
server list — cost ~420 ms. emitState() runs on every adapter change, across
nine wallets, so the main thread was never given back.
Three changes:
- txCache is capped at 400 entries, pruned newest-first by block time
(unconfirmed entries sort as newest — they are the current ones). Worst
case ~0.3 MB per wallet instead of unbounded.
- TX_CACHE_VERSION 3, so existing oversized caches are discarded on first
load rather than needing a manual clear.
- loadHistory() only persists when something actually changed; an idle wallet
was rewriting the whole file on every poll for nothing.
- api.storage gains a write-through read cache, so repeated gets cost one
parse per process instead of one per call. Writes still go to the host
unchanged. Safe because this process is the only writer — Aegis's panel
talks over addon messages and never touches addon storage; if that changes,
the cache has to go.
window.bitcoincash was gated on a ".x" hostname in both the preload and the
host handlers — inherited from the pre-multi-wallet build, when every dapp
Aegis knew about was a Silent Mode one. It was never a security boundary: the
TRX / ETH / SOL bridges have always injected everywhere, and every BCH call
still crosses into main, raises an approval overlay and honours per-origin
permissions. The preload cannot read an address or sign anything by itself.
What the gate actually cost was Cauldron, bch.guru and our own dapps on other
TLDs (potidaea.asm) seeing no BCH provider at all, on a wallet whose entire
subject is BCH. WizardConnect was never gated this way, so the two halves of
the same bridge disagreed about who could talk to it.
isBchOrigin becomes isDappOrigin and now only keeps non-web schemes out —
file://, chrome://, data:, javascript: and anything unparseable.
Separately: page-inject declared https://*/* only, so a dapp served from
http://127.0.0.1 got no preload at all, which is a different cause from the
.x gate and would have survived removing it. localhost, 127.0.0.1 and [::1]
are now matched over http as well (the host's compiler makes the port
optional, so :3000 and :5173 are covered). Plain http on any other host stays
out, since an http page is tamperable in transit and only local development
needs the exception.
Two regressions in 0.26.0.
The PIN was still asked after the wallet had painted. pinGateOnLoad() ran
after render() and was not awaited, so the interface appeared — balances and
all — and the pad then opened on top of a panel the user could already read.
It is now awaited before the first render, and `pinGateBlocked` keeps the
chrome hidden for as long as the PIN is owed: render() returns early into a
closed door, state pushes cannot paint around it, and Settings is covered too
(unlike the vault lock, where Settings must stay reachable to create a PIN in
the first place, here it would just be a way around the gate). Cancelling
leaves the door shut with a retry rather than falling through to an open
wallet.
The pad threw digits away mid-entry. render() fires on every state push —
balance polls fire it every couple of seconds — and renderLockScreen()
rebuilt body.innerHTML unconditionally, replacing the pad and its buffer
under the user's fingers. The reset was timed to the poll, not to the
keypress count, which is why it looked like "after two digits". All three
branches are now keyed on body.dataset.mode and rebuild only when the mode
actually changes; the master-password and first-run forms had the same defect
and were erasing half-typed input the same way.
Also: every pad was wired through getElementById, so two pads alive at once
(a load gate plus a transaction gate) were duplicate ids and the second
setupPinPad bound the first pad's buttons. All four are now scoped to their
own container, only one PIN prompt can be open at a time, and setupPinPad
carries a note saying why it must never be handed a global lookup.
Reported: after a restart Aegis opens without asking for a PIN, then demands
one as soon as you click something. That came from the only control being
requirePinForSending — safeStorage remembers the master password, so the
wallet reopens unlocked, and the PIN prompt then ambushes the first action.
Asking at the door is coherent. Asking nothing is coherent. Asking once the
user is already inside is not.
"Ask for PIN" is now four independent triggers, because wanting one at
startup and again per transaction is a normal combination:
- On each browser restart (compares a per-process boot id; a timestamp
cannot tell a restart from a long idle)
- On each wallet launch (hide/show — one panel load is one launch)
- Every 6 hours
- Each transaction
With none ticked, an open wallet is never interrupted again. The decision is
made host-side: the panel reloads on every hide/show and must not be the
thing that remembers a gate was cleared. A clearance is only recorded after
the panel has actually decrypted the PIN blob, which is proof rather than a
claim, and ticking a trigger does not fire it retroactively.
restart defaults ON for a wallet that otherwise reopens fully unlocked, and
transaction inherits the old requirePinForSending so nobody loses a gate they
had chosen. Removing the PIN clears every trigger and the clearance record.