From deab66fe0eb4c1fe8bd34c26baca40134e3875cc Mon Sep 17 00:00:00 2001 From: Local Dev Date: Wed, 23 Sep 2026 00:16:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(theseus+aegis):=20WizardConnect=20auto-det?= =?UTF-8?q?ection=20=E2=80=94=20wiz://=20links=20+=20page=20scan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- addon.json | 4 ++-- index.js | 14 ++++++++++++++ panel.js | 40 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/addon.json b/addon.json index 7c8edf0..59cfa0c 100644 --- a/addon.json +++ b/addon.json @@ -1,14 +1,14 @@ { "id": "aegis", "name": "Aegis Wallet", - "version": "0.8.9", + "version": "0.9.0", "category": "plugin", "description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites; window.tronWeb / window.tronLink / window.ethereum / window.solana on any https page.", "author": "Silent Mode", "icon": "data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='none'%3E%3Cpolygon points='16,2 28,9 28,23 16,30 4,23 4,9' fill='%230a0a0d' stroke='%23D6FF3D' stroke-width='1.6' stroke-linejoin='round'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='none' stroke='%23D6FF3D' stroke-width='1.4'/%3E%3Ccircle cx='16' cy='16' r='1.6' fill='%23D6FF3D'/%3E%3C/svg%3E", "main": "index.js", "updateURL": "https://navigate.st/bns/theseus.x/extensions/aegis/updates.json", - "capabilities": ["sidebar-panel", "vault-derive", "page-inject", "approval-modal"], + "capabilities": ["sidebar-panel", "vault-derive", "page-inject", "approval-modal", "scan-page"], "absorbs": ["bchwallet", "siawallet"], "page-inject": { "preload": "wallet-inject.js", diff --git a/index.js b/index.js index c6ca833..002e684 100644 --- a/index.js +++ b/index.js @@ -1585,6 +1585,20 @@ function registerPanelMessages(api) { return fullState(); }); + // Scan the open dapp tab for a wiz:// pairing code, for dapps that render + // one but haven't adopted window.wizardconnect. Strictly user-initiated — + // it runs when someone presses "Scan page", never on a timer and never in + // the background. The host does the matching and returns only the URIs, so + // Aegis never receives page content. + api.onMessage("wcScanPage", async (_p, m) => { + fromPanel(m); + if (typeof api.scanActiveTabForUris !== "function") { + throw new Error("This Theseus build can't scan pages yet — update Theseus, or paste the wiz:// code manually."); + } + const { origin, uris } = await api.scanActiveTabForUris({ scheme: "wiz", limit: 10 }); + return { origin: origin || null, uris: Array.isArray(uris) ? uris : [] }; + }); + // ---- WizardConnect from the page (0.8.8) -------------------------------- // // WC was built for cross-device pairing: the dapp renders a QR, a phone diff --git a/panel.js b/panel.js index 6df3079..cdb5f42 100644 --- a/panel.js +++ b/panel.js @@ -1892,7 +1892,7 @@ function renderConnectPane(bchWallets) { `).join("") : `
No dapps paired yet.
`; return `
-
Dapps that support Aegis directly can hand the pairing over with one click — no copying. Otherwise paste a wiz:// URI from the dapp's Connect dialog. Aegis signs every request after your approval.
+
Dapps that support Aegis hand the pairing over with one click. Otherwise open the dapp's Connect dialog and press Scan page, or paste its wiz:// code below. Aegis signs every request after your approval.
${blockedNote}
Sign with
@@ -1901,8 +1901,9 @@ function renderConnectPane(bchWallets) {
-
+
+
Paired dapps
@@ -1948,6 +1949,41 @@ function wireConnectPane() { fillPicker(); } catch (e) { msg.textContent = cleanErr(e); msg.hidden = false; } }); + const scanBtn = document.getElementById("pkConnectScanBtn"); + if (scanBtn) scanBtn.addEventListener("click", async (e) => { + e.stopPropagation(); + const msg = document.getElementById("pkConnectMsg"); msg.hidden = true; + const field = document.getElementById("pkConnectUri"); + const prev = scanBtn.textContent; + scanBtn.textContent = "Scanning…"; scanBtn.disabled = true; + try { + const res = await S.invoke("wcScanPage"); + const uris = res?.uris || []; + if (!uris.length) { + msg.textContent = res?.origin + ? `No wiz:// pairing code found on ${res.origin}. Open the dapp's Connect dialog first, then scan again.` + : "No pairing code found on the open tab."; + msg.hidden = false; + return; + } + // Fill the field rather than pairing outright: the user still picks + // which wallet signs, and still presses Connect. A scan that silently + // paired would be a click with a much larger consequence than the + // button implies. + field.value = uris[0]; + msg.textContent = uris.length > 1 + ? `Found ${uris.length} codes on ${res.origin || "the page"} — filled the first. Press Connect to pair.` + : `Found a pairing code on ${res.origin || "the page"}. Press Connect to pair.`; + msg.classList.remove("err"); + msg.hidden = false; + } catch (err) { + msg.textContent = cleanErr(err); + msg.classList.add("err"); + msg.hidden = false; + } finally { + scanBtn.textContent = prev; scanBtn.disabled = false; + } + }); document.querySelectorAll("[data-wcpick]").forEach((b) => b.addEventListener("click", async () => { const [walletId, connId] = b.dataset.wcpick.split("|"); try { state = await S.invoke("wcDisconnect", { walletId, connId }); fillPicker(); }