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.
This commit is contained in:
parent
64bc26ecf6
commit
8174fccba0
5 changed files with 834 additions and 664 deletions
1350
addons-host.js
1350
addons-host.js
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1892,7 +1892,7 @@ function renderConnectPane(bchWallets) {
|
|||
</div>`).join("")
|
||||
: `<div class="hint" style="padding:6px 8px">No dapps paired yet.</div>`;
|
||||
return `<div style="padding:6px">
|
||||
<div class="hint" style="margin-bottom:8px">Dapps that support Aegis directly can hand the pairing over with one click — no copying. Otherwise paste a <span class="mono">wiz://</span> URI from the dapp's Connect dialog. Aegis signs every request after your approval.</div>
|
||||
<div class="hint" style="margin-bottom:8px">Dapps that support Aegis hand the pairing over with one click. Otherwise open the dapp's Connect dialog and press <b>Scan page</b>, or paste its <span class="mono">wiz://</span> code below. Aegis signs every request after your approval.</div>
|
||||
${blockedNote}
|
||||
<div class="field">
|
||||
<div class="lbl">Sign with</div>
|
||||
|
|
@ -1901,8 +1901,9 @@ function renderConnectPane(bchWallets) {
|
|||
<div class="field">
|
||||
<input type="text" id="pkConnectUri" spellcheck="false" placeholder="wiz://?p=…&s=…">
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="actions" style="gap:6px">
|
||||
<button class="btn primary" id="pkConnectBtn">Connect</button>
|
||||
<button class="btn" id="pkConnectScanBtn" title="Look for a wiz:// pairing code on the dapp tab you have open">Scan page</button>
|
||||
</div>
|
||||
<div class="msg err" id="pkConnectMsg" hidden></div>
|
||||
<div class="lbl" style="margin-top:14px">Paired dapps</div>
|
||||
|
|
@ -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(); }
|
||||
|
|
|
|||
90
main.js
90
main.js
|
|
@ -1860,6 +1860,59 @@ function initAddons() {
|
|||
// and-stitch pass; kept for a later revision.
|
||||
// region — run the caller-supplied overlay source in the tab, wait
|
||||
// for a rect (or null = cancel), then capturePage(rect).
|
||||
// Back scan-page. The extraction runs IN the page and returns only the
|
||||
// matched URIs — the add-on never sees the DOM. We look at anchor hrefs,
|
||||
// visible text, and the handful of attributes a dapp realistically
|
||||
// stashes a pairing code in (data-uri, value, title), then dedupe.
|
||||
//
|
||||
// WizardConnect also has a QR-alphanumeric form (WIZ://%3FP%3D…), which
|
||||
// is often the ONLY thing in the DOM when a dapp renders a QR, so the
|
||||
// matcher accepts the percent-encoded spelling too and decodes it.
|
||||
scanTabForUris: async ({ scheme, limit }, addonId) => {
|
||||
const t = activeTab();
|
||||
if (!t) throw new Error("no active tab");
|
||||
if (t.addonId || t.settings) throw new Error("open the dapp's tab first, then scan");
|
||||
const wc = t.view.webContents;
|
||||
const origin = pageOriginOf(wc.getURL());
|
||||
// The regex SOURCES are built here and shipped as JSON. Assembling
|
||||
// them inside the injected string instead means hand-escaping
|
||||
// backslashes and quotes through two levels of literal, which is both
|
||||
// easy to get wrong and unreviewable. JSON.stringify does it exactly.
|
||||
// `scheme` is already validated against [a-z][a-z0-9+.-]* upstream, so
|
||||
// it cannot carry regex metacharacters.
|
||||
const plainSrc = `\\b${scheme}://[^\\s"'<>]{4,2048}`;
|
||||
// Percent-encoded QR spelling: WIZ://%3FP%3D…
|
||||
const qrSrc = `\\b${scheme}://(?:%[0-9A-Fa-f]{2}|[A-Za-z0-9._~$+-])+`;
|
||||
const arg = JSON.stringify({ plainSrc, qrSrc, limit });
|
||||
const found = await wc.executeJavaScript(`(() => {
|
||||
const { plainSrc, qrSrc, limit } = ${arg};
|
||||
const out = new Set();
|
||||
const plain = new RegExp(plainSrc, "gi");
|
||||
const qr = new RegExp(qrSrc, "gi");
|
||||
const push = (s) => {
|
||||
if (!s || out.size >= limit) return;
|
||||
let v = String(s).trim();
|
||||
if (v.includes("%3F") || v.includes("%3f")) { try { v = decodeURIComponent(v); } catch {} }
|
||||
if (v.length <= 2048) out.add(v);
|
||||
};
|
||||
const scan = (s) => {
|
||||
if (!s) return;
|
||||
for (const m of String(s).matchAll(plain)) push(m[0]);
|
||||
for (const m of String(s).matchAll(qr)) push(m[0]);
|
||||
};
|
||||
for (const a of document.querySelectorAll("a[href]")) scan(a.getAttribute("href"));
|
||||
for (const el of document.querySelectorAll("[data-uri],[data-wc-uri],[value],[title]")) {
|
||||
scan(el.getAttribute("data-uri")); scan(el.getAttribute("data-wc-uri"));
|
||||
scan(el.getAttribute("value")); scan(el.getAttribute("title"));
|
||||
}
|
||||
for (const el of document.querySelectorAll("input,textarea")) scan(el.value);
|
||||
scan(document.body ? document.body.innerText : "");
|
||||
return [...out].slice(0, limit);
|
||||
})()`, true);
|
||||
const uris = Array.isArray(found) ? found.filter((s) => typeof s === "string") : [];
|
||||
console.log(`[addons] ${addonId} scanned ${origin || "tab"} for ${scheme}:// — ${uris.length} match(es)`);
|
||||
return { origin, uris };
|
||||
},
|
||||
captureTab: async (opts, addonId) => {
|
||||
// Prefer the currently-active tab, BUT if that's an add-on-owned page
|
||||
// (e.g. the screenshot editor is already up when the user re-picks a
|
||||
|
|
@ -2692,6 +2745,13 @@ function createTab(initial, opts = {}) {
|
|||
}
|
||||
return navigateTab(id, target + rest);
|
||||
}
|
||||
// A wiz:// click never navigates — it hands the pairing URI to the
|
||||
// wallet and leaves the dapp exactly where it is.
|
||||
if (parsed.protocol === "wiz:") {
|
||||
e.preventDefault();
|
||||
routeWizUri(u, pageOriginOf(wc.getURL()), tab.id);
|
||||
return;
|
||||
}
|
||||
if (parsed.protocol === "bns:") return;
|
||||
if (isBnsHost(parsed.hostname)) {
|
||||
// Only intercept cross-origin navigations. Same-origin (a form submit
|
||||
|
|
@ -2733,6 +2793,12 @@ function createTab(initial, opts = {}) {
|
|||
});
|
||||
// Links that open a new tab: target="_blank", window.open, Ctrl/middle-click.
|
||||
wc.setWindowOpenHandler(({ url, disposition }) => {
|
||||
// target="_blank" on a wiz:// link lands here rather than will-navigate.
|
||||
// Route it to the wallet instead of opening a tab on an unloadable URL.
|
||||
if (url && /^wiz:/i.test(url)) {
|
||||
routeWizUri(url, pageOriginOf(wc.getURL()), tab.id);
|
||||
return { action: "deny" };
|
||||
}
|
||||
if (url && url !== "about:blank") createTab(url, { background: disposition === "background-tab" });
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
|
@ -3417,6 +3483,30 @@ function pageOriginOf(url) {
|
|||
return u.protocol && u.host ? `${u.protocol}//${u.host}` : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
// wiz:// — WizardConnect pairing links.
|
||||
//
|
||||
// WizardConnect is a cross-device protocol: a dapp renders its pairing URI
|
||||
// as a QR for a phone wallet to scan. On the same device that means copying
|
||||
// a wiz:// string out of one tab and pasting it into the wallet by hand.
|
||||
// When a dapp renders the URI as a link instead, we can route the click
|
||||
// straight to the wallet — no copying, and no change required on the dapp
|
||||
// side beyond making it an anchor, so this works for third-party dapps that
|
||||
// will never adopt a Silent Mode API.
|
||||
//
|
||||
// The wallet still shows its own approval before anything is paired; all
|
||||
// this does is carry the URI across, tagged with the origin that offered it
|
||||
// so the approval can name the real site.
|
||||
function routeWizUri(uri, origin, tabId) {
|
||||
if (!addonHost) return false;
|
||||
const u = String(uri || "");
|
||||
if (!/^wiz:/i.test(u) || u.length > 4096) return false;
|
||||
if (!addonHost.hasHandler("aegis", "wcConnectFromPage")) return false;
|
||||
addonHost
|
||||
.dispatch("aegis", "wcConnectFromPage", { uri: u }, { from: "page", origin: origin || "unknown site", tabId })
|
||||
.catch((err) => console.log("[wiz] pairing failed:", err?.message || err));
|
||||
return true;
|
||||
}
|
||||
|
||||
ipcMain.handle("addon-page-msg", async (e, addonId, msg, payload) => {
|
||||
const tab = tabForSender(e.sender);
|
||||
if (!tab || !addonHost) throw new Error("not a page");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue