feat(theseus): one-click "Install in Theseus" from theseus.x/extensions

The extensions page could only hand out tarballs; installing meant going
to Settings › Extensions › Community and finding the entry again. Pages
now get window.bcnr.installExtension(id) and Theseus intercepts
theseus://extensions/install/<id> links (page clicks, target=_blank and
the address bar). The page only names a catalog id: Theseus fetches the
catalog and package itself, asks in a native dialog the page cannot draw
over, verifies the publisher signature against the name's current owner
and activates the add-on — the same path a Settings install takes. One
prompt at a time; an already-installed version says so instead of
offering a no-op update.

The site shows the button inside Theseus (feature-detected on the
bridge), a "update Theseus" hint on older builds and a download hint in
other browsers.
This commit is contained in:
Local Dev 2026-09-21 02:40:05 +02:00
parent 1086046212
commit be36492d8a

View file

@ -96,6 +96,14 @@
.ext .h b{color:var(--mut);font-weight:600;min-width:70px} .ext .h b{color:var(--mut);font-weight:600;min-width:70px}
.ext .h code{font-family:ui-monospace,monospace;font-size:10.5px;color:#c9d4e3;word-break:break-all;background:transparent;border:none;padding:0} .ext .h code{font-family:ui-monospace,monospace;font-size:10.5px;color:#c9d4e3;word-break:break-all;background:transparent;border:none;padding:0}
.ext .meta{color:var(--dim);font-size:12.5px;margin-top:.4rem} .ext .meta{color:var(--dim);font-size:12.5px;margin-top:.4rem}
.ext .act{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:.8rem}
.ext .install{display:inline-block;background:var(--acid);color:#0b0e14;font-weight:600;font-size:14px;
border:none;border-radius:9px;padding:9px 18px;cursor:pointer;text-decoration:none;font-family:inherit}
.ext .install:hover{filter:brightness(1.08)}
.ext .install[disabled]{opacity:.6;cursor:default}
.ext .install.alt{background:rgba(214,255,61,.12);color:var(--acid)}
.ext .act .st{font-size:13px;color:var(--mut)}
.ext .act .st.ok{color:var(--acid)} .ext .act .st.err{color:#f6768a}
.note{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--acid); .note{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--acid);
border-radius:10px;padding:14px 18px;color:var(--mut);font-size:14px;margin:1.4rem 0} border-radius:10px;padding:14px 18px;color:var(--mut);font-size:14px;margin:1.4rem 0}
.note b{color:var(--ink)} .note b{color:var(--ink)}
@ -190,9 +198,10 @@
<section id="community"> <section id="community">
<h2>Community extensions</h2> <h2>Community extensions</h2>
<p class="lede">Published by BCDN name holders, signed with the wallet that holds the name. Theseus installs them <p class="lede">Published by BCDN name holders, signed with the wallet that holds the name. Open this page in Theseus
from <b>Settings › Extensions › Community</b> and re-checks every signature against the name's current owner and click <b>Install in Theseus</b> — or use <b>Settings › Extensions › Community</b>. Either way Theseus fetches the
in its own chain index — the relay cannot substitute code under a trusted name.</p> package itself and re-checks every signature against the name's current owner in its own chain index — the relay
cannot substitute code under a trusted name.</p>
<div id="communityList"><div class="ext" style="color:var(--dim)">Loading the catalog…</div></div> <div id="communityList"><div class="ext" style="color:var(--dim)">Loading the catalog…</div></div>
<div class="note"><b>Trust, stated plainly.</b> A community extension runs with the same access as any add-on — <div class="note"><b>Trust, stated plainly.</b> A community extension runs with the same access as any add-on —
treat one like an unsigned executable from that publisher. The signature proves <i>who</i> published it, treat one like an unsigned executable from that publisher. The signature proves <i>who</i> published it,
@ -254,6 +263,16 @@
// Community catalog // Community catalog
const box = document.getElementById("communityList"); const box = document.getElementById("communityList");
const esc = (s) => String(s || "").replace(/[&<>"']/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" })[c]); const esc = (s) => String(s || "").replace(/[&<>"']/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" })[c]);
// Inside Theseus every page has window.bcnr; builds since 0.3.50 add
// installExtension(id). The page only passes the catalog id — Theseus
// fetches the package, confirms in a native dialog and verifies the signature.
const inTheseus = !!window.bcnr;
const canInstall = inTheseus && typeof window.bcnr.installExtension === "function";
const installUi = (id) => canInstall
? `<button class="install" type="button" data-install="${esc(id)}">Install in Theseus</button><span class="st" data-st="${esc(id)}"></span>`
: inTheseus
? `<a class="install alt" href="../#download">Update Theseus to install from here</a><span class="st">until then: Settings › Extensions › Community › Install</span>`
: `<a class="install alt" href="../#download">Get Theseus to install</a><span class="st">one click from this page inside Theseus</span>`;
try { try {
const r = await fetch("./community/catalog.json", { cache: "no-store" }); const r = await fetch("./community/catalog.json", { cache: "no-store" });
const cat = r.ok ? await r.json() : { extensions: [] }; const cat = r.ok ? await r.json() : { extensions: [] };
@ -264,14 +283,22 @@
<h3><span class="icon">${esc(e.icon || "🧩")}</span> ${esc(e.name)} <span class="cur" style="margin-left:auto">v${esc(e.latest)}</span></h3> <h3><span class="icon">${esc(e.icon || "🧩")}</span> ${esc(e.name)} <span class="cur" style="margin-left:auto">v${esc(e.latest)}</span></h3>
<div class="desc">${esc(e.description)}</div> <div class="desc">${esc(e.description)}</div>
<div class="meta">Published by <b>${esc(e.publisher)}</b> · ${esc(String(e.versions || 1))} version${e.versions === 1 ? "" : "s"} · updated ${esc(String(e.updatedAt || "").slice(0, 10))}${e.capabilities && e.capabilities.length ? " · " + esc(e.capabilities.join(", ")) : ""}</div> <div class="meta">Published by <b>${esc(e.publisher)}</b> · ${esc(String(e.versions || 1))} version${e.versions === 1 ? "" : "s"} · updated ${esc(String(e.updatedAt || "").slice(0, 10))}${e.capabilities && e.capabilities.length ? " · " + esc(e.capabilities.join(", ")) : ""}</div>
<div class="act">${installUi(e.id)}</div>
<div class="verifybox"> <div class="verifybox">
<div class="lbl">Publisher-signed channel — verified by Theseus against ${esc(e.publisher)}'s current owner</div> <div class="lbl">Publisher-signed channel — verified by Theseus against ${esc(e.publisher)}'s current owner</div>
<div class="h"><b>updates.json</b><code><a href="${esc(e.updatesUrl)}">${esc(String(e.updatesUrl).replace(/^https?:\/\/[^/]+\/bns\//, ""))}</a></code></div> <div class="h"><b>updates.json</b><code><a href="${esc(e.updatesUrl)}">${esc(String(e.updatesUrl).replace(/^https?:\/\/[^/]+\/bns\//, ""))}</a></code></div>
<div class="h"><b>Tarball</b><code><a href="${esc(e.tarballUrl)}">${esc(String(e.tarballUrl).split("/").pop())}</a></code></div> <div class="h"><b>Tarball</b><code><a href="${esc(e.tarballUrl)}">${esc(String(e.tarballUrl).split("/").pop())}</a></code></div>
<div class="h"><b>sha256</b><code>${esc(e.sha256)}</code></div> <div class="h"><b>sha256</b><code>${esc(e.sha256)}</code></div>
<div class="h"><b>Install</b><code>Theseus › Settings › Extensions › Community › Install</code></div> <div class="h"><b>Link</b><code>theseus://extensions/install/${esc(e.id)}</code></div>
</div> </div>
</div>`).join(""); </div>`).join("");
box.querySelectorAll("button[data-install]").forEach((btn) => btn.addEventListener("click", async () => {
const id = btn.dataset.install, st = box.querySelector(`[data-st="${CSS.escape(id)}"]`);
btn.disabled = true; st.className = "st"; st.textContent = "Confirm in the Theseus dialog…";
let r; try { r = await window.bcnr.installExtension(id); } catch (err) { r = { ok: false, error: err?.message || String(err) }; }
if (r && r.ok) { st.className = "st ok"; st.textContent = `Installed v${r.version} — signed by ${r.publisher}`; btn.textContent = "Installed"; }
else { btn.disabled = false; st.className = r?.error === "cancelled" ? "st" : "st err"; st.textContent = r?.error === "cancelled" ? "" : (r?.error || "install failed"); }
}));
} catch (e) { box.innerHTML = '<div class="ext" style="color:var(--dim)">Catalog unavailable right now.</div>'; } } catch (e) { box.innerHTML = '<div class="ext" style="color:var(--dim)">Catalog unavailable right now.</div>'; }
})(); })();
</script> </script>