The trust note said a community extension runs with the same access as any add-on. It now runs in its own process with read access to its own folder only; the note says what that does and does not cover, and the build steps list what an author has to change (promises, bundled dependencies, no request filters). To be deployed with the Theseus release that ships the sandbox, not before.
499 lines
34 KiB
HTML
499 lines
34 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>theseus.x — Extensions</title>
|
||
<meta name="description" content="Signed over-the-air update endpoint for Theseus's bundled add-ons. Each entry is Ed25519-signed by the Silent Mode operator; verified on every install without waiting for a Theseus release.">
|
||
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml">
|
||
<link rel="mask-icon" href="../assets/favicon.svg" color="#d6ff3d">
|
||
<meta property="og:type" content="website">
|
||
<meta property="og:site_name" content="theseus.x">
|
||
<meta property="og:title" content="theseus.x — Extensions">
|
||
<meta property="og:description" content="Signed OTA update endpoint for Theseus's bundled add-ons. Ed25519, no cert authority, no app-store gatekeeper.">
|
||
<meta property="og:url" content="https://theseus.x/extensions/">
|
||
<style>
|
||
:root{
|
||
--bg:#0b0e14; --panel:#141a24; --panel2:#18202c; --line:rgba(255,255,255,.09);
|
||
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
|
||
--ok:#4fd1a5;
|
||
}
|
||
*{box-sizing:border-box}
|
||
body{margin:0;background:radial-gradient(1100px 560px at 50% -12%,#16202e,var(--bg));
|
||
color:var(--ink);font:16px/1.65 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-height:100vh}
|
||
a{color:var(--acid)}
|
||
.wrap{max-width:900px;margin:0 auto;padding:0 1.2rem}
|
||
.topnav{position:fixed;top:0;left:0;right:0;z-index:20;display:flex;gap:2px;align-items:center;flex-wrap:wrap;
|
||
padding:10px 1.2rem;background:rgba(11,14,20,.88);backdrop-filter:blur(10px)}
|
||
.topnav .brand{font-weight:600;color:var(--ink);text-decoration:none;margin-right:14px;font-size:15px;letter-spacing:.2px;display:flex;align-items:center;gap:8px}
|
||
.topnav .brand .g{color:var(--acid)}
|
||
.topnav .brand svg{width:20px;height:20px}
|
||
.topnav a{color:var(--mut);text-decoration:none;padding:6px 12px;border-radius:8px;font-size:14px;
|
||
display:inline-flex;align-items:center;gap:6px}
|
||
.topnav a:hover{color:var(--ink);background:var(--panel)}
|
||
.topnav a.here{color:var(--acid);background:rgba(214,255,61,.08)}
|
||
.topnav a .g{color:var(--acid)}
|
||
.topnav a svg.tabicon{width:15px;height:15px;flex:none}
|
||
|
||
/* Mobile hamburger — matches the home-page pattern. */
|
||
.topnav .menu{display:none}
|
||
@media (max-width:720px){
|
||
.topnav{flex-wrap:nowrap;padding:10px 1rem}
|
||
.topnav .brand{margin-right:0}
|
||
.topnav > a:not(.brand){display:none}
|
||
.topnav .menu{display:block;margin-left:auto;position:relative}
|
||
.topnav .menu > summary{list-style:none;cursor:pointer;padding:8px 10px;
|
||
border-radius:8px;color:var(--mut);display:inline-flex;align-items:center;gap:6px}
|
||
.topnav .menu > summary::-webkit-details-marker{display:none}
|
||
.topnav .menu > summary svg{width:22px;height:22px;display:block}
|
||
.topnav .menu > summary:hover,
|
||
.topnav .menu[open] > summary{color:var(--ink);background:var(--panel)}
|
||
.topnav .menu .menu-panel{position:absolute;top:calc(100% + 6px);right:0;
|
||
background:var(--bg);border:1px solid var(--line);border-radius:12px;
|
||
padding:6px;min-width:220px;display:flex;flex-direction:column;gap:2px;
|
||
box-shadow:0 10px 32px rgba(0,0,0,.55)}
|
||
.topnav .menu .menu-panel a{padding:10px 14px;border-radius:8px;font-size:14.5px;
|
||
color:var(--mut);text-decoration:none;display:flex;align-items:center;gap:8px}
|
||
.topnav .menu .menu-panel a:hover{color:var(--ink);background:var(--panel)}
|
||
}
|
||
.topnav a.get{margin-left:auto;background:rgba(214,255,61,.10);color:var(--acid);font-weight:500}
|
||
.topnav a.get:hover{background:rgba(214,255,61,.18)}
|
||
|
||
/* Adaptive edge nav: full-width panel at left edge on wide,
|
||
icon rail on medium, bottom bar with icons on mobile. */
|
||
:root{--sn-w:200px;--tn-h:52px}
|
||
body{padding-top:var(--tn-h)}
|
||
.sidenav{position:fixed;top:var(--tn-h);left:0;bottom:0;width:var(--sn-w);z-index:15;
|
||
background:transparent;padding:56px 12px 22px 18px;
|
||
overflow-y:auto;transition:width .15s ease;pointer-events:none}
|
||
.sidenav a{display:flex;align-items:center;gap:14px;color:var(--mut);text-decoration:none;
|
||
padding:9px 12px;border-radius:10px;
|
||
font-size:14.5px;font-weight:500;opacity:.35;pointer-events:auto;
|
||
transition:opacity .12s,color .12s,background .12s,backdrop-filter .12s}
|
||
.sidenav a .ic{flex:none;width:22px;font-size:18px;line-height:1;text-align:center;
|
||
display:none;justify-content:center;align-items:center}
|
||
.sidenav a .lb{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.sidenav a:hover{opacity:1;color:var(--ink);background:rgba(11,14,20,.72);backdrop-filter:blur(8px)}
|
||
.sidenav a.active{opacity:.85;color:var(--acid)}
|
||
.sidenav a.active:hover{opacity:1}
|
||
@media (max-width:1180px){
|
||
:root{--sn-w:64px}
|
||
.sidenav{padding:22px 6px}
|
||
.sidenav .lbl{display:none}
|
||
.sidenav a{justify-content:center;padding:12px 4px;gap:0}
|
||
.sidenav a .ic{display:inline-flex}
|
||
.sidenav a .lb{display:none}
|
||
}
|
||
@media (max-width:720px){
|
||
:root{--sn-w:0px}
|
||
body{padding-left:0;padding-bottom:64px}
|
||
.sidenav{top:auto;bottom:0;left:0;right:0;width:100%;height:auto;
|
||
padding:4px 4px;flex-direction:row;background:rgba(11,14,20,.52);
|
||
display:flex;justify-content:space-around;overflow-x:auto}
|
||
.sidenav .lbl{display:none}
|
||
.sidenav a{flex:1;min-width:56px;padding:10px 6px;justify-content:center;gap:0}
|
||
.sidenav a .ic{display:inline-flex}
|
||
.sidenav a .lb{display:none}
|
||
}
|
||
|
||
header.hero{text-align:center;padding:3.5rem 1.2rem 1rem}
|
||
h1{font-size:clamp(1.8rem,4.5vw,2.4rem);margin:.6rem 0 .3rem;letter-spacing:-.3px}
|
||
h1 .g{color:var(--acid)}
|
||
.tag{color:var(--mut);font-size:1.02rem;max-width:640px;margin:0 auto}
|
||
section{padding:2rem 0;border-top:1px solid var(--line);margin-top:1.6rem;scroll-margin-top:72px}
|
||
section:first-of-type{border-top:none;margin-top:1rem}
|
||
h2{font-size:1.35rem;margin:0 0 .5rem}
|
||
.lede{color:var(--mut);margin:0 0 1.2rem;max-width:720px}
|
||
code,.mono{background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-family:ui-monospace,monospace;font-size:13px}
|
||
pre{background:#0e131b;border:1px solid var(--line);border-radius:10px;padding:12px 14px;overflow-x:auto;
|
||
font-family:ui-monospace,monospace;font-size:12.5px;color:var(--ink);margin:.8rem 0}
|
||
.ext{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px 22px;margin:1rem 0}
|
||
.ext h3{margin:0 0 .3rem;font-size:1.15rem;display:flex;align-items:center;gap:10px}
|
||
.ext h3 .icon{font-size:1.4rem}
|
||
.ext .cur{color:var(--acid);font-size:12px;font-weight:600;letter-spacing:.14em;text-transform:uppercase}
|
||
.ext .desc{color:var(--mut);font-size:14px;margin:.3rem 0 .8rem}
|
||
.ext .verifybox{margin-top:.9rem;padding-top:.8rem;border-top:1px solid var(--line);font-size:11.5px;color:var(--dim)}
|
||
.ext .verifybox .lbl{color:var(--mut);font-size:12px;margin-bottom:6px}
|
||
.ext .h{display:flex;gap:8px;flex-wrap:wrap;align-items:baseline;margin-top:4px}
|
||
.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 .meta{color:var(--dim);font-size:12.5px;margin-top:.4rem}
|
||
.ext .act .download{display:inline-block;padding:9px 14px;border-radius:9px;text-decoration:none;
|
||
font-size:13.5px;font-weight:600;background:transparent;color:var(--ink);border:1px solid var(--line)}
|
||
.ext .act .download:hover{background:var(--panel)}
|
||
.ext .act .hint{color:var(--dim);font-size:11.5px;margin-left:auto;flex-basis:100%}
|
||
.ext .act .hint b{color:var(--mut);font-weight:500}
|
||
.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}
|
||
.ext .act .how{flex-basis:100%;font-size:13px;color:var(--mut);background:#0e131b;border:1px solid var(--line);border-radius:8px;padding:8px 12px}
|
||
.ext .how b{color:var(--ink)}
|
||
.search{max-width:560px;margin:1.4rem auto 0;position:relative}
|
||
.search input{width:100%;background:var(--panel);border:1px solid var(--line);border-radius:12px;color:var(--ink);
|
||
font:inherit;font-size:15px;padding:11px 14px 11px 40px;outline:none}
|
||
.search input:focus{border-color:rgba(214,255,61,.5)}
|
||
.search svg{position:absolute;left:13px;top:50%;transform:translateY(-50%);width:17px;height:17px;color:var(--dim)}
|
||
.search .n{position:absolute;right:13px;top:50%;transform:translateY(-50%);font-size:12px;color:var(--dim)}
|
||
.ext.hide, .nomatch{display:none}
|
||
.nomatch.show{display:block}
|
||
.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}
|
||
.note b{color:var(--ink)}
|
||
.pubkey{background:#0e131b;border:1px solid var(--line);border-radius:8px;padding:10px 12px;
|
||
font-family:ui-monospace,monospace;font-size:11px;color:#c9d4e3;word-break:break-all;margin:.6rem 0}
|
||
.flow{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 20px;margin-top:1rem;color:var(--mut);font-size:14px}
|
||
.flow ol{margin:0;padding-left:20px}
|
||
.flow li{margin:.4rem 0}
|
||
.flow b{color:var(--ink)}
|
||
footer{color:var(--dim);font-size:12.5px;margin:3rem 0 2rem;text-align:center}
|
||
footer a{color:var(--mut)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="topnav">
|
||
<a class="brand" href="../">
|
||
<svg viewBox="0 0 64 64" aria-hidden="true">
|
||
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
|
||
<path d="M32 6 L38 32 L26 32 Z" fill="#e04e4e"/>
|
||
<path d="M38 32 L32 58 L26 32 Z" fill="#d6ff3d"/>
|
||
<path d="M6 32 L32 27 L58 32 L32 37 Z" fill="#d6ff3d" opacity=".55"/>
|
||
<circle cx="32" cy="32" r="3" fill="#0b0e14"/>
|
||
</svg>
|
||
<span>Theseus<span class="g">.X</span></span>
|
||
</a>
|
||
<a href="../plug-ins/">Plug-ins</a>
|
||
<a class="here" href="./">Extensions</a>
|
||
<a href="../dev/">Developers</a>
|
||
<a href="//sirius.x/" title="Register a BCNR name"><svg class="tabicon" viewBox="0 0 64 64" aria-hidden="true"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M32 4 L36.4 27.6 L60 32 L36.4 36.4 L32 60 L27.6 36.4 L4 32 L27.6 27.6 Z" fill="#d6ff3d"/></svg><span>Sirius<span class="g">.X</span></span></a>
|
||
<a class="get" href="../#download">Get Theseus</a>
|
||
<details class="menu">
|
||
<summary aria-label="Open menu">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg>
|
||
</summary>
|
||
<div class="menu-panel">
|
||
<a href="../plug-ins/">Plug-ins</a>
|
||
<a href="./">Extensions</a>
|
||
<a href="../dev/">Developers</a>
|
||
<a href="//sirius.x/">Sirius.X</a>
|
||
</div>
|
||
</details>
|
||
</nav>
|
||
|
||
<aside class="sidenav" aria-label="On this page">
|
||
<a href="#bundled" title="Bundled" ><span class="ic" aria-hidden="true">📦</span><span class="lb">Bundled</span></a>
|
||
<a href="#community" title="Community" ><span class="ic" aria-hidden="true">👥</span><span class="lb">Community</span></a>
|
||
<a href="#build" title="Build & publish" ><span class="ic" aria-hidden="true">🔨</span><span class="lb">Build & publish</span></a>
|
||
<a href="#updates" title="How updates work" ><span class="ic" aria-hidden="true">🔄</span><span class="lb">How updates work</span></a>
|
||
<a href="#operator-key" title="Operator key" ><span class="ic" aria-hidden="true">🔑</span><span class="lb">Operator key</span></a>
|
||
<a href="#roadmap" title="Roadmap" ><span class="ic" aria-hidden="true">🗺</span><span class="lb">Roadmap</span></a>
|
||
</aside>
|
||
|
||
<header class="hero">
|
||
<h1>Theseus <span class="g">Extensions</span></h1>
|
||
<p class="tag">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 <a href="../plug-ins/">own page</a>.</p>
|
||
<div class="search">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
|
||
<input id="extSearch" type="search" placeholder="Search extensions — name, what it does, publisher" autocomplete="off" aria-label="Search extensions">
|
||
<span class="n" id="extSearchN"></span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="wrap">
|
||
|
||
<section id="bundled">
|
||
<h2>Bundled extensions</h2>
|
||
<p class="lede">Ship with every Theseus build and update over the operator-signed channel. Versions and hashes
|
||
are read from each add-on's signed <code>updates.json</code> when this page loads.</p>
|
||
|
||
<div class="ext" id="screenshot" data-ext="screenshot">
|
||
<h3><span class="icon">📸</span> Screenshot <span class="cur" style="margin-left:auto">current <span data-live="version">0.6.5</span></span></h3>
|
||
<div class="desc">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.</div>
|
||
<div class="meta">Settings › Extensions · <span data-live="count"></span></div>
|
||
<div class="act">
|
||
<a class="install" href="theseus://extensions/install/screenshot" data-install="screenshot">Install in Theseus</a>
|
||
<a class="download" data-live="download" href="https://navigate.st/bns/theseus.x/extensions/screenshot/screenshot-0.6.5.tar.gz" download>Download tarball</a>
|
||
<div class="hint">Updates: signed <a href="./screenshot/updates.json">updates.json</a> · SHA-256 verified before every install</div>
|
||
</div>
|
||
<div class="verifybox">
|
||
<div class="lbl">Signed update manifest — SHA-256 verified before install</div>
|
||
<div class="h"><b>updates.json</b><code><a href="./screenshot/updates.json">theseus.x/extensions/screenshot/updates.json</a></code></div>
|
||
<div class="h"><b>Tarball</b><code><a data-live="url" href="./screenshot/screenshot-0.6.5.tar.gz">screenshot-0.6.5.tar.gz</a></code></div>
|
||
<div class="h"><b>sha256</b><code data-live="sha256">87ae730d329ff9f552bbf513efba19bd14bb016bb90e24f7b62868434dadda60</code></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ext" id="notepad">
|
||
<h3><span class="icon">📝</span> Notepad <span class="cur" style="margin-left:auto">bundled 0.1.0</span></h3>
|
||
<div class="desc">Right-sidebar notes. Text lives on this machine only and autosaves as you type.</div>
|
||
<div class="meta">Settings › Extensions · no update channel yet — ships with each Theseus release.</div>
|
||
<div class="act">
|
||
<span class="hint" style="margin-left:0"><b>Ships with Theseus</b> — nothing to install; appears under Settings › Extensions on first launch.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ext" id="translate">
|
||
<h3><span class="icon">🌐</span> Translate <span class="cur" style="margin-left:auto">bundled 0.1.0</span></h3>
|
||
<div class="desc">Select text on any page and right-click <i>Translate</i> — the sidebar panel shows the translation,
|
||
with LibreTranslate or Google as the backend (your choice in the panel). Nothing is sent until you ask.</div>
|
||
<div class="meta">Settings › Extensions · no update channel yet — ships with each Theseus release.</div>
|
||
<div class="act">
|
||
<span class="hint" style="margin-left:0"><b>Ships with Theseus</b> — nothing to install; appears under Settings › Extensions on first launch.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ext" id="pdf-editor" data-ext="pdf-editor">
|
||
<h3><span class="icon" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style="vertical-align:middle"><path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z" fill="#dc2626"/><path d="M14 2v6h6z" fill="#991b1b"/><text x="12" y="17" text-anchor="middle" font-family="system-ui,-apple-system,sans-serif" font-size="5.5" font-weight="800" fill="#fff">PDF</text></svg></span> PDF Editor <span class="cur" style="margin-left:auto">current <span data-live="version">0.1.0</span></span></h3>
|
||
<div class="desc">Open a PDF in a full tab — read, highlight, mark up, reorder or rotate or drop pages,
|
||
fill form fields, save a new copy. Annotations are written as real PDF content, the original
|
||
is never touched. Opens from the toolbar's PDF Editor menu or the right-click <i>Open PDF in editor</i>
|
||
on any link.</div>
|
||
<div class="meta">Settings › Extensions · <span data-live="count">signed update channel</span></div>
|
||
<div class="act">
|
||
<a class="install" href="theseus://extensions/install/pdf-editor" data-install="pdf-editor">Install in Theseus</a>
|
||
<a class="download" data-live="download" href="https://navigate.st/bns/theseus.x/extensions/pdf-editor/pdf-editor-0.1.0.tar.gz" download>Download tarball</a>
|
||
<div class="hint">Updates: signed <a href="./pdf-editor/updates.json">updates.json</a> · SHA-256 verified before every install</div>
|
||
</div>
|
||
<div class="verifybox">
|
||
<div class="lbl">Signed update manifest — SHA-256 verified before install</div>
|
||
<div class="h"><b>updates.json</b><code><a href="./pdf-editor/updates.json">theseus.x/extensions/pdf-editor/updates.json</a></code></div>
|
||
<div class="h"><b>Tarball</b><code><a data-live="url" href="./pdf-editor/pdf-editor-0.1.0.tar.gz">pdf-editor-0.1.0.tar.gz</a></code></div>
|
||
<div class="h"><b>sha256</b><code data-live="sha256">—</code></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="nomatch note" id="nomatchBundled">No bundled extension matches that search.</div>
|
||
</section>
|
||
|
||
<section id="community">
|
||
<h2>Community extensions</h2>
|
||
<p class="lede">Published by BCDN name holders, signed with the wallet that holds the name. Open this page in Theseus
|
||
and click <b>Install in Theseus</b> — or use <b>Settings › Extensions › Community</b>. Either way Theseus fetches the
|
||
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 class="nomatch note" id="nomatchCommunity">No community extension matches that search.</div>
|
||
<div class="note"><b>Trust, stated plainly.</b> The signature proves <i>who</i> published an extension, not that it
|
||
is safe. A community extension runs in its own sandboxed process: it can read only its own folder, cannot start
|
||
other programs, and cannot see your vault, your wallet or the browser's memory. It reaches Theseus only through
|
||
the capabilities its manifest declares, and it <i>can</i> use the network. Its panel and anything it injects
|
||
into pages still do what those capabilities allow, so read the capability list before installing. Older Theseus
|
||
builds ran extensions inside the browser itself — keep Theseus up to date.</div>
|
||
</section>
|
||
|
||
<section id="build">
|
||
<h2>Build and publish your own</h2>
|
||
<p class="lede">An extension is a folder with an <code>addon.json</code> 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.</p>
|
||
<div class="flow">
|
||
<ol>
|
||
<li><b>Write it.</b> The manifest and a CommonJS entry that exports <code>activate(api)</code>:
|
||
<pre>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</pre>
|
||
The bundled <a href="https://code.silentmode.st/silentmode/theseus/src/branch/master/TheseusNavigator/bundled-addons/notepad">Notepad</a>
|
||
is the smallest complete example; <a href="https://code.silentmode.st/silentmode/theseus/src/branch/master/TheseusNavigator/addons-host.js">addons-host.js</a>
|
||
is the full API. <code>id</code> is <code>[a-z0-9._-]</code>, 2–64 chars, and belongs to the first name that publishes it.</li>
|
||
<li><b>Mind the sandbox.</b> Your main script runs in its own process, not inside the browser:
|
||
<ul>
|
||
<li>It can read its own folder and write to <code>api.dataDir</code>, nothing else on disk. No child processes, workers or native modules.</li>
|
||
<li>Calls into Theseus return promises, <code>api.tabs.active()</code> included. <code>api.storage.get</code> stays synchronous.</li>
|
||
<li><code>api.require</code> and <code>api.import</code> are not available: bundle the modules you need in the package.</li>
|
||
<li>Request filters and site routes are for built-in add-ons. The vault is reachable only as <code>api.vault.derive</code> under your own id and <code>api.vault.requestUnlock</code>, where Theseus draws the prompt.</li>
|
||
<li><code>api.features.sandboxed</code> is <code>true</code> when this applies.</li>
|
||
</ul></li>
|
||
<li><b>Test it locally.</b> Settings › Extensions › <i>Open extensions folder</i>, drop the folder in, <i>Reload</i>.</li>
|
||
<li><b>Package it.</b> <code>tar -czf my-ext-1.0.0.tar.gz -C my-ext .</code> (max 8 MB; <code>addon.json</code> at the root or in a single top folder).</li>
|
||
<li><b>Publish it.</b> Open <a class="btn" href="./publish/" style="display:inline-block;padding:.35rem .8rem;border-radius:8px;background:var(--acid);color:#0b0e14;font-weight:650;text-decoration:none">Publish an extension →</a>
|
||
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.</li>
|
||
</ol>
|
||
</div>
|
||
<p class="lede" style="margin-top:1rem">Prefer the command line? The upload is one signed <code>PUT</code>:
|
||
<code>PUT https://silentmode.st/api/ext/<your-name>/<id>/<version></code> with the tarball as body and headers
|
||
<code>x-bns-ts</code>, <code>x-bns-sig</code> (over <code>sha256("BNS-EXT1\n<name>\n<id>\n<version>\n<sha256>\n<ts>")</code>)
|
||
and <code>x-bns-entry-sig</code> (over <code>sha256("silentmode.extension-v1|<id>|<version>|<sha256>|<name>")</code>),
|
||
both 65-byte BCH message signatures by the name's owner key.</p>
|
||
</section>
|
||
|
||
<script>
|
||
// Bundled cards: fill from each add-on's signed manifest (relative URLs work on every origin the page is served from).
|
||
(async () => {
|
||
const cmp = (a, b) => { const x = String(a).split(".").map(Number), y = String(b).split(".").map(Number); for (let i = 0; i < Math.max(x.length, y.length); i++) { const d = (x[i] || 0) - (y[i] || 0); if (d) return d; } return 0; };
|
||
for (const card of document.querySelectorAll(".ext[data-ext]")) {
|
||
const id = card.dataset.ext;
|
||
try {
|
||
const r = await fetch("./" + id + "/updates.json", { cache: "no-store" });
|
||
if (!r.ok) continue;
|
||
const m = await r.json();
|
||
const list = (m.addons || []).slice().sort((x, y) => cmp(y.version, x.version));
|
||
const top = list[0]; if (!top) continue;
|
||
const set = (k, f) => card.querySelectorAll('[data-live="' + k + '"]').forEach(f);
|
||
set("version", (el) => { el.textContent = top.version; });
|
||
set("sha256", (el) => { el.textContent = top.sha256 || "—"; });
|
||
set("url", (el) => { el.href = top.url; el.textContent = String(top.url).split("/").pop(); });
|
||
set("download", (el) => { if (top.url) el.href = top.url; });
|
||
set("count", (el) => { el.textContent = list.length + " signed version" + (list.length === 1 ? "" : "s") + " on the channel"; });
|
||
} catch {}
|
||
}
|
||
// Community catalog
|
||
const box = document.getElementById("communityList");
|
||
const esc = (s) => String(s || "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[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 mode = canInstall ? "bridge" : inTheseus ? "old" : "none";
|
||
const installUi = (id) => mode === "none"
|
||
? `<a class="install" href="../#download">Get Theseus to install</a><span class="st">one click from this page inside Theseus</span>`
|
||
: `<button class="install" type="button" data-install="${esc(id)}" data-mode="${mode}">Install in Theseus</button><span class="st" data-st="${esc(id)}"></span>`;
|
||
try {
|
||
const r = await fetch("./community/catalog.json", { cache: "no-store" });
|
||
const cat = r.ok ? await r.json() : { extensions: [] };
|
||
const list = Array.isArray(cat.extensions) ? cat.extensions : [];
|
||
if (!list.length) { box.innerHTML = '<div class="ext" style="color:var(--dim)">Nothing published yet — yours could be the first. See <a href="#build">Build and publish your own</a>.</div>'; return; }
|
||
const iconHtml = (icon) => {
|
||
const s = String(icon || "🧩");
|
||
if (/^data:image\//i.test(s)) return `<img src="${esc(s)}" alt="" style="width:20px;height:20px;vertical-align:middle">`;
|
||
return esc(s);
|
||
};
|
||
box.innerHTML = list.map((e) => `
|
||
<div class="ext" id="ext-${esc(e.id)}">
|
||
<h3><span class="icon">${iconHtml(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="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="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>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>Link</b><code>theseus://extensions/install/${esc(e.id)}</code></div>
|
||
</div>
|
||
</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)}"]`);
|
||
if (btn.dataset.mode === "old") {
|
||
// This Theseus predates the page bridge: point at the path it does have.
|
||
const name = [...btn.closest(".ext").querySelector("h3").childNodes].find((x) => x.nodeType === 3 && x.textContent.trim())?.textContent.trim() || id;
|
||
let how = btn.parentElement.querySelector(".how");
|
||
if (!how) { how = document.createElement("div"); how.className = "how"; btn.parentElement.appendChild(how); }
|
||
how.innerHTML = `Your Theseus is older than this button. Open <b>Settings › Extensions</b>, scroll to <b>Community</b> and press
|
||
<b>Install</b> next to <b>${esc(name)}</b> — same signed package, same checks. Newer builds install from here in one click:
|
||
<a href="../#download">update Theseus</a>.`;
|
||
return;
|
||
}
|
||
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>'; }
|
||
})();
|
||
// Search: filters the bundled and community cards by name, description, publisher, capabilities.
|
||
(() => {
|
||
const q = document.getElementById("extSearch"), n = document.getElementById("extSearchN");
|
||
const apply = () => {
|
||
const s = q.value.trim().toLowerCase();
|
||
let shown = 0;
|
||
for (const sec of ["bundled", "community"]) {
|
||
const cards = [...document.querySelectorAll(`#${sec} .ext`)].filter((c) => c.querySelector("h3"));
|
||
let hit = 0;
|
||
for (const c of cards) { const ok = !s || c.textContent.toLowerCase().includes(s); c.classList.toggle("hide", !ok); if (ok) hit++; }
|
||
document.getElementById(sec === "bundled" ? "nomatchBundled" : "nomatchCommunity").classList.toggle("show", !!s && cards.length > 0 && hit === 0);
|
||
shown += hit;
|
||
}
|
||
n.textContent = s ? `${shown} match${shown === 1 ? "" : "es"}` : "";
|
||
};
|
||
q.addEventListener("input", apply);
|
||
new MutationObserver(() => { if (q.value.trim()) apply(); }).observe(document.getElementById("communityList"), { childList: true });
|
||
if (location.hash === "#search") q.focus();
|
||
})();
|
||
</script>
|
||
|
||
<section id="updates">
|
||
<h2>How updates work</h2>
|
||
<p class="lede">Same mechanism a Firefox XPI update uses, cut down to what a name-native browser needs.</p>
|
||
<div class="flow">
|
||
<ol>
|
||
<li>Every 30 seconds after boot Theseus fetches <code>updates.json</code> for each installed add-on that declares an <code>updateURL</code> in its manifest.</li>
|
||
<li>Each entry carries an Ed25519 signature over <code>silentmode.addon-update-v1|<id>|<version>|<sha256></code>. Theseus verifies against a small set of operator public keys baked into the build. Unsigned or badly-signed entries are dropped silently.</li>
|
||
<li>If a signed version beats what's installed, Theseus downloads the tarball, re-checks its sha256, and stages the new copy under <code><userData>/addons-updates-staged/<id>-<version>/</code>.</li>
|
||
<li>On the next Theseus launch, <code>promoteStagedUpdates</code> moves the staged copy into <code>addons/<id>/</code>. Any prior copy is renamed to <code>addons-backups/<id>-<oldver>-<timestamp>/</code> so hand-edits survive.</li>
|
||
<li>Settings › Extensions has a <b>Check for updates</b> button and shows what is staged; first-party plug-ins (Aegis) can also stage and restart from their own panel.</li>
|
||
</ol>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="operator-key">
|
||
<h2>Operator key</h2>
|
||
<p class="lede">The Ed25519 public key baked into Theseus 0.3.19+.
|
||
Rotation is by adding a new pubkey and shipping a Theseus release that carries both, then a follow-up release removing the old — during the overlap, entries can be signed by either.</p>
|
||
<div class="pubkey">732b1263a236b0030383a2376597cfa43c3624b3ca2912a46134f8f2a06e6012</div>
|
||
<p style="color:var(--dim);font-size:12.5px;margin-top:.6rem">Generated 2026-09-07. Held offline by the Silent Mode operator.</p>
|
||
</section>
|
||
|
||
<section id="roadmap">
|
||
<h2>Roadmap</h2>
|
||
<p class="lede">The endpoint is generic — any bundled add-on can advertise an <code>updateURL</code>.
|
||
Concrete next-steps:</p>
|
||
<ul style="color:var(--mut);font-size:14px;line-height:1.7">
|
||
<li>On-BCNR pubkey publication so rotation is auditable end-to-end.</li>
|
||
<li>Multi-sig (N-of-M operator keys) so a single compromised private key can't push code.</li>
|
||
<li>Extend the channel to <b>Notepad</b> (bundled, no channel yet).</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<footer>
|
||
Silent Mode · <a href="https://code.silentmode.st/silentmode/theseus/src/branch/master/TheseusNavigator/docs/ADDON-UPDATES.md">source & operator brief</a>
|
||
</footer>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
// Sidenav scroll-spy: highlight the section currently in view.
|
||
// Uses scroll-position tracking so short sections at the end of the page
|
||
// still activate their link when the reader scrolls to them.
|
||
(() => {
|
||
const links = document.querySelectorAll(".sidenav a[href^='#']");
|
||
if (!links.length) return;
|
||
const entries = [];
|
||
links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) entries.push({ el, link: a }); });
|
||
if (!entries.length) return;
|
||
const setActive = (link) => { links.forEach((l) => l.classList.remove("active")); link.classList.add("active"); };
|
||
const update = () => {
|
||
const doc = document.documentElement;
|
||
const scrollBottom = window.scrollY + window.innerHeight;
|
||
if (scrollBottom >= (doc.scrollHeight - 4)) { setActive(entries[entries.length - 1].link); return; }
|
||
const line = window.scrollY + window.innerHeight * 0.35;
|
||
let active = entries[0].link;
|
||
for (const e of entries) {
|
||
if (e.el.getBoundingClientRect().top + window.scrollY <= line) active = e.link;
|
||
else break;
|
||
}
|
||
setActive(active);
|
||
};
|
||
window.addEventListener("scroll", update, { passive: true });
|
||
window.addEventListener("resize", update);
|
||
update();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|