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: