From 8fb940ec808f375e897e65fa23e1f0fee02e5ba8 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 20 Sep 2026 15:26:30 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20community=20extensions=20=E2=80=94=20pu?= =?UTF-8?q?blish=20with=20a=20BCDN=20name,=20install=20from=20Settings,=20?= =?UTF-8?q?theseus.x=20catalog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anyone who owns a BCDN name can now publish a Theseus extension, and every Theseus can install it with the publisher's signature verified locally. Gateway (Argus/src/gateway/public-gateway.mjs): PUT /api/ext/// takes the gzipped tar, checks two BCH message signatures against the name's current NFT owner (one authorises the upload, one is stored in the channel), inspects the package (addon.json at the root, id/version/main match, 8 MB cap), enforces first-publisher ownership of an id and monotonic versions, and writes the tarball, the extension's updates.json and community/catalog.json to Sia. GET /api/ext/catalog reads the catalog back with CORS. Theseus: lib/publisher-sig.mjs recovers the signer of a channel entry; main.js compares it with the publisher name's owner from Theseus's own chain index before installing or updating, so neither the relay nor a tampered catalog can pass off code under a trusted name. addon-updater.js gains installCommunity() and accepts publisher-signed entries in the regular update check (operator Ed25519 entries unchanged). Settings › Extensions shows the community catalog with Install / Update; Settings › Plug-ins links to theseus.x/plug-ins. theseus.x: /plug-ins/ is a separate page for the first-party plug-ins (Aegis, Ariadne's Thread) with live versions and hashes; /extensions/ lists the bundled extensions, the community catalog, and how to build and publish; /extensions/publish/ signs and uploads a package in the browser with the wallet that holds the publisher's name (session helper + wallet bundle copied alongside). --- extensions/index.html | 110 +++++++++++----- extensions/js/bns-register.js | 7 + extensions/js/session.js | 141 ++++++++++++++++++++ extensions/publish/index.html | 239 ++++++++++++++++++++++++++++++++++ index.html | 1 + plug-ins/index.html | 177 +++++++++++++++++++++++++ 6 files changed, 643 insertions(+), 32 deletions(-) create mode 100644 extensions/js/bns-register.js create mode 100644 extensions/js/session.js create mode 100644 extensions/publish/index.html create mode 100644 plug-ins/index.html diff --git a/extensions/index.html b/extensions/index.html index 6c859c3..ada19f2 100644 --- a/extensions/index.html +++ b/extensions/index.html @@ -82,46 +82,32 @@ Home Download + Plug-ins Extensions sirius.x

Theseus Extensions

-

Signed over-the-air update endpoint for Theseus's bundled add-ons. - Every entry is Ed25519-signed by the Silent Mode operator, verified on the - device before install. No cert authority, no app-store gatekeeper.

+

Add-ons for Theseus Navigator — the ones that ship in the box and the ones anyone can + publish. Every package is signed (by the Silent Mode operator, or by the publisher's own BCDN name) + and verified on your device before install. No cert authority, no app-store gatekeeper. + First-party plug-ins (Aegis, Ariadne's Thread) have their own page.

-
-

Available extensions

-

What ships inside Theseus today. Versions and hashes on this page are read from each - add-on's signed updates.json when the page loads, so they match what the browser - would install. First-party components show up under Settings › Plug-ins in Theseus, - everything else under Settings › Extensions.

- -
-

🛡️ Aegis current 0.6.21

-
Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault, with - a dApp bridge for the pages you visit. Bundled; updates over this signed channel and through the - panel's own "Stage → Restart" flow.
-
Plug-in · Settings › Plug-ins ·
-
-
Signed update manifest — SHA-256 verified before install
- - -
sha256—
-
-
+
+

Bundled extensions

+

Ship with every Theseus build and update over the operator-signed channel. Versions and hashes + are read from each add-on's signed updates.json when this page loads.

📸 Screenshot current 0.6.5

Capture the current tab — visible viewport, entire scrollable page, or a rectangle you draw. The capture opens in a full-tab editor: crop, annotate, line tool, resizable text, redact, save.
-
Extension · Settings › Extensions ·
+
Settings › Extensions ·
Signed update manifest — SHA-256 verified before install
@@ -133,19 +119,58 @@

📝 Notepad bundled 0.1.0

Right-sidebar notes. Text lives on this machine only and autosaves as you type.
-
Extension · Settings › Extensions · no update channel yet — ships with each Theseus release.
+
Settings › Extensions · no update channel yet — ships with each Theseus release.
+
-
Installing something else. An extension is a folder with an addon.json. - Drop it into the extensions directory (Settings › Extensions › Open extensions folder), press - Reload, and it is live. Extensions run with full app access — treat one like an unsigned executable - and load only what you trust.
+
+

Community extensions

+

Published by BCDN name holders, signed with the wallet that holds the name. Theseus installs them + from Settings › Extensions › Community 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.

+
Loading the catalog…
+
Trust, stated plainly. A community extension runs with the same access as any add-on — + treat one like an unsigned executable from that publisher. The signature proves who published it, + not that it is safe.
+
+ +
+

Build and publish your own

+

An extension is a folder with an addon.json and a main script. Package it, sign it with + the wallet that holds one of your BCDN names, and it is listed above within a minute. Nothing to apply for.

+
+
    +
  1. Write it. The manifest and a CommonJS entry that exports activate(api): +
    my-ext/
    +  addon.json      {"id":"my-ext","name":"My Extension","version":"1.0.0",
    +                   "description":"…","author":"you.bch","icon":"✨",
    +                   "main":"index.js","capabilities":["sidebar-panel"]}
    +  index.js        module.exports = { activate(api) {
    +                    api.registerSidebarPanel({ id:"main", title:"My Extension", icon:"✨", page:"panel.html" });
    +                  } };
    +  panel.html      plain HTML, runs in the sidebar; window.silentmode.storage for local state
    + The bundled Notepad + is the smallest complete example; addons-host.js + is the full API. id is [a-z0-9._-], 2–64 chars, and belongs to the first name that publishes it.
  2. +
  3. Test it locally. Settings › Extensions › Open extensions folder, drop the folder in, Reload.
  4. +
  5. Package it. tar -czf my-ext-1.0.0.tar.gz -C my-ext . (max 8 MB; addon.json at the root or in a single top folder).
  6. +
  7. Publish it. Open Publish an extension → + sign in with the recovery phrase of the wallet that holds your name, pick the package, and sign. The gateway + verifies the signature against the name's on-chain owner, stores the package on Sia, and updates the catalog. + New versions: same page, higher version number, same name.
  8. +
+
+

Prefer the command line? The upload is one signed PUT: + PUT https://silentmode.st/api/ext/<your-name>/<id>/<version> with the tarball as body and headers + x-bns-ts, x-bns-sig (over sha256("BNS-EXT1\n<name>\n<id>\n<version>\n<sha256>\n<ts>")) + and x-bns-entry-sig (over sha256("silentmode.extension-v1|<id>|<version>|<sha256>|<name>")), + both 65-byte BCH message signatures by the name's owner key.

diff --git a/extensions/js/bns-register.js b/extensions/js/bns-register.js new file mode 100644 index 0000000..46da9b1 --- /dev/null +++ b/extensions/js/bns-register.js @@ -0,0 +1,7 @@ +import{generateBip39Mnemonic as qe,deriveSeedFromBip39Mnemonic as ne,deriveHdPrivateNodeFromSeed as Ke,deriveHdPath as We,secp256k1 as oe,hash160 as ze,encodeCashAddress as no,lockingBytecodeToCashAddress as Ge,cashAddressToLockingBytecode as oo,binToHex as je,hexToBin as so,utf8ToBin as ie,binToBase64 as Nt,base64ToBin as Dt}from"@bitauth/libauth";var mt="bchtest";var $t="m/44'/145'/0'",Je=5,kt="bns.wallet.v1";function ae(e){let t=ze(e);return Uint8Array.from([118,169,20,...t,136,172])}function se(e,t,n){let o=Ge({bytecode:ae(e),prefix:t,tokenSupport:n});if(typeof o=="string")throw new Error(`address encoding failed: ${o}`);return o.address}var Rt=class e{constructor(t,n,o=mt,s=$t){this.mnemonic=t,this.prefix=o,this.accountPath=s,this._root=Ke(n),this._keys=new Map,this._derive()}static async create(t=mt,n=$t){let o=qe();return new e(o,await ne(o),t,n)}static async fromMnemonic(t,n=mt,o=$t){let s=String(t).trim().toLowerCase().replace(/\s+/g," "),r=await ne(s);if(typeof r=="string")throw new Error(`invalid recovery phrase: ${r}`);return new e(s,r,n,o)}_derive(){this.receive=[],this.change=[];for(let[t,n]of[[0,this.receive],[1,this.change]])for(let o=0;ot.address)}keyFor(t){let n=this._keys.get(t);return n?{privateKey:n.privateKey,publicKey:n.publicKey}:null}signMessage(t,n){if(!(t instanceof Uint8Array)||t.length!==32)throw new Error("signMessage expects a 32-byte Uint8Array digest");let o=n||this.address,s=[...this.receive,...this.change].find(h=>h.address===o);if(!s)throw new Error(`no key for address ${o}`);let r=oe.signMessageHashRecoverableCompact(s.privateKey,t);if(typeof r=="string")throw new Error(`signMessage failed: ${r}`);let{signature:a}=r,i=r.recoveryId??r.recovery;if(typeof i!="number")throw new Error("signMessage: no recovery id in signature result");let l=31+i,c=new Uint8Array(65);return c[0]=l,c.set(a,1),c}async encrypt(t){let n=crypto.getRandomValues(new Uint8Array(16)),o=crypto.getRandomValues(new Uint8Array(12)),s=await re(t,n),r=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:o},s,ie(this.mnemonic)));return JSON.stringify({v:1,kdf:"PBKDF2-SHA256",iterations:Ft,prefix:this.prefix,salt:Nt(n),iv:Nt(o),ct:Nt(r)})}static async decrypt(t,n){let o=typeof t=="string"?JSON.parse(t):t,s=await re(n,Dt(o.salt),o.iterations??Ft),r;try{r=await crypto.subtle.decrypt({name:"AES-GCM",iv:Dt(o.iv)},s,Dt(o.ct))}catch{throw new Error("wrong password")}return e.fromMnemonic(new TextDecoder().decode(r),o.prefix??mt)}async save(t,n=globalThis.localStorage){n.setItem(kt,await this.encrypt(t))}static async load(t,n=globalThis.localStorage){let o=n.getItem(kt);return o?e.decrypt(o,t):null}static exists(t=globalThis.localStorage){return!!t?.getItem(kt)}static forget(t=globalThis.localStorage){t.removeItem(kt)}},Ft=25e4;async function re(e,t,n=Ft){let o=await crypto.subtle.importKey("raw",ie(String(e)),"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({name:"PBKDF2",salt:t,iterations:n,hash:"SHA-256"},o,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}var bt=["wss://silentmode.st/electrum","wss://chipnet.bch.ninja:50004","wss://chipnet.imaginary.cash:50004","wss://tbch4.loping.net:62002"],Lt=class{constructor(t,n){this.url=t,this.WS=n||globalThis.WebSocket,this.id=0,this.pending=new Map,this.buf=""}connect(){return new Promise((t,n)=>{let o=new this.WS(this.url);this.ws=o;let s=async()=>{try{await this.call("server.version",["bns-web","1.4.2"]),t(this)}catch(i){n(i)}},r=i=>n(new Error("electrum ws error: "+(i?.message||this.url))),a=i=>this._onData(typeof i.data=="string"?i.data:i.data.toString());o.addEventListener?(o.addEventListener("open",s),o.addEventListener("error",r),o.addEventListener("message",a)):(o.onopen=s,o.onerror=r,o.onmessage=a)})}_handle(t){let n=this.pending.get(t.id);n&&(this.pending.delete(t.id),t.error?n.reject(new Error(JSON.stringify(t.error))):n.resolve(t.result))}_onData(t){this.buf+=t;let n;for(;(n=this.buf.indexOf(` +`))>=0;){let s=this.buf.slice(0,n).trim();if(this.buf=this.buf.slice(n+1),s)try{this._handle(JSON.parse(s))}catch{}}let o=this.buf.trim();if(o)try{let s=JSON.parse(o);this._handle(s),this.buf=""}catch{}}call(t,n=[]){let o=++this.id;return new Promise((s,r)=>{this.pending.set(o,{resolve:s,reject:r}),this.ws.send(JSON.stringify({id:o,method:t,params:n})+` +`),setTimeout(()=>{this.pending.has(o)&&(this.pending.delete(o),r(new Error(`electrum timeout: ${t}`)))},2e4)})}close(){try{this.ws.close()}catch{}}};async function ce(e=bt,t){let n=[];for(let o of e)try{return await new Lt(o,t).connect()}catch(s){n.push(`${o}: ${s?.message??s}`)}throw Pe(e.length,n)}function Pe(e,t){let n=new Error(`no electrum server reachable (tried ${e}). These use non-standard ports, which some networks and VPNs block: + ${t.join(` + `)}`);return n.code="NO_ELECTRUM",n.failures=t,n}import{cashAddressToLockingBytecode as Ye,lockingBytecodeToCashAddress as Xe,sha256 as Ve,hash160 as co,binToHex as Qe,hexToBin as lo}from"@bitauth/libauth";function le(e){return Qe(Ve.hash(e).slice().reverse())}function _(e){let t=Ye(e);if(typeof t=="string")throw new Error(`bad address "${e}": ${t}`);return t.bytecode}function Ze(e){return le(_(e))}function M(e){let t=_(e),n=String(e).includes(":")?e.split(":")[0]:"bchtest",o=Xe({bytecode:t,prefix:n,tokenSupport:!0});if(typeof o=="string")throw new Error(`cannot token-encode "${e}": ${o}`);return o.address}async function ue(e,t){let n=_(t),o=le(n);return(await e.call("blockchain.scripthash.listunspent",[o,"include_tokens"])).map(r=>({txid:r.tx_hash,vout:r.tx_pos,satoshis:BigInt(r.value),height:r.height,address:t,lockingBytecode:n,token:r.token_data?{category:r.token_data.category,amount:BigInt(r.token_data.amount??0),nft:r.token_data.nft?{capability:r.token_data.nft.capability,commitment:r.token_data.nft.commitment}:void 0}:void 0}))}async function z(e,t){let n=[];for(let o of t)n.push(...await ue(e,o));return n}async function V(e,t){return e.call("blockchain.transaction.broadcast",[t])}import{binToHex as vt,hexToBin as j,utf8ToBin as st,binToUtf8 as de,encodeTransaction as tn,decodeTransaction as po}from"@bitauth/libauth";var en="BNS1",Tt="bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl",Mt="bchtest:qzc4c76vtw9nd49g6x8nmvc0jz62uvk4dc7hmpmerh",I=600n,F=1000n,U=546n,Ct="bch",Ht=200,nn=/^[a-z0-9-]{1,32}$/,he=/^[a-z0-9-]{1,16}$/;function ht(e){let t=String(e).toLowerCase().trim().replace(/^\.+/,"").replace(/\.+$/,"");if(!he.test(t))throw new Error(`invalid TLD "${e}" (allowed: [a-z0-9-]{1,16})`);return t}var St=class extends Error{constructor(t){super(t),this.name="NoGenesisInputError",this.code="NO_GENESIS_INPUT"}},J=class extends Error{constructor(t,{needSats:n,haveSats:o}={}){super(t),this.name="InsufficientFundsError",this.code="INSUFFICIENT_FUNDS",this.needSats=n,this.haveSats=o}};function P(e){let t=String(e).toLowerCase().trim().replace(/\.$/,"").split(".").filter(Boolean),n,o;if(t.length===0)throw new Error(`invalid name "${e}"`);if(t.length===1?(n=t[0],o=Ct):(o=t[t.length-1],n=t[t.length-2]),!nn.test(n)||!he.test(o))throw new Error(`invalid name "${e}" (allowed: