The connector ships off by default with the next Theseus release; users need the setup steps (claude mcp add with the panel's token) and a plain account of what it can and cannot do before turning it on.
512 lines
35 KiB
HTML
512 lines
35 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="ext" id="claude-connector">
|
||
<h3><span class="icon" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" style="vertical-align:middle"><rect x="4" y="4" width="56" height="56" rx="14" fill="#d97757"/><path d="M32 13v38M13 32h38M18.6 18.6l26.8 26.8M45.4 18.6 18.6 45.4" stroke="#fff4ee" stroke-width="6" stroke-linecap="round"/></svg></span> Claude connector <span class="cur" style="margin-left:auto">bundled 0.1.0</span></h3>
|
||
<div class="desc">Lets Claude Code or Claude Desktop on your computer work in your signed-in tabs through a
|
||
local MCP connection: read pages, click, type into rich editors, paste formatted text, upload files.
|
||
Off until you turn it on. Theseus asks before Claude touches each new site, never lets it type into
|
||
a password field, uploads only from folders you choose, and logs every action. A <i>Claude</i> chip
|
||
in the toolbar shows while a client is connected; its ✕ stops everything.</div>
|
||
<div class="meta">Settings › Extensions · off by default · ships with the next Theseus release (after 0.3.106)</div>
|
||
<div class="act">
|
||
<span class="hint" style="margin-left:0"><b>Ships with Theseus</b> — nothing to install. Setup: <a href="../dev/#claude">theseus.x/dev/#claude</a>.</span>
|
||
</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>
|