feat: community extensions — publish with a BCDN name, install from Settings, theseus.x catalog
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/<name>/<id>/<version> 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).
This commit is contained in:
parent
1ee9cc423e
commit
8fb940ec80
6 changed files with 643 additions and 32 deletions
|
|
@ -82,46 +82,32 @@
|
|||
</a>
|
||||
<a href="/">Home</a>
|
||||
<a href="/#download">Download</a>
|
||||
<a href="/plug-ins/">Plug-ins</a>
|
||||
<a class="here" href="./">Extensions</a>
|
||||
<a href="https://sirius.x/">sirius.x</a>
|
||||
</nav>
|
||||
|
||||
<header class="hero">
|
||||
<h1>Theseus <span class="g">Extensions</span></h1>
|
||||
<p class="tag">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.</p>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<section>
|
||||
<h2>Available extensions</h2>
|
||||
<p class="lede">What ships inside Theseus today. Versions and hashes on this page are read from each
|
||||
add-on's signed <code>updates.json</code> when the page loads, so they match what the browser
|
||||
would install. First-party components show up under <b>Settings › Plug-ins</b> in Theseus,
|
||||
everything else under <b>Settings › Extensions</b>.</p>
|
||||
|
||||
<div class="ext" id="aegis" data-ext="aegis">
|
||||
<h3><span class="icon">🛡️</span> Aegis <span class="cur" style="margin-left:auto">current <span data-live="version">0.6.21</span></span></h3>
|
||||
<div class="desc">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.</div>
|
||||
<div class="meta">Plug-in · Settings › Plug-ins · <span data-live="count"></span></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="./aegis/updates.json">theseus.x/extensions/aegis/updates.json</a></code></div>
|
||||
<div class="h"><b>Tarball</b><code><a data-live="url" href="./aegis/aegis-0.6.21.tar.gz">aegis-0.6.21.tar.gz</a></code></div>
|
||||
<div class="h"><b>sha256</b><code data-live="sha256">—</code></div>
|
||||
</div>
|
||||
</div>
|
||||
<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">Extension · Settings › Extensions · <span data-live="count"></span></div>
|
||||
<div class="meta">Settings › Extensions · <span data-live="count"></span></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>
|
||||
|
|
@ -133,19 +119,58 @@
|
|||
<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">Extension · Settings › Extensions · no update channel yet — ships with each Theseus release.</div>
|
||||
<div class="meta">Settings › Extensions · no update channel yet — ships with each Theseus release.</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="note"><b>Installing something else.</b> An extension is a folder with an <code>addon.json</code>.
|
||||
Drop it into the extensions directory (Settings › Extensions › <i>Open extensions folder</i>), press
|
||||
<i>Reload</i>, and it is live. Extensions run with full app access — treat one like an unsigned executable
|
||||
and load only what you trust.</div>
|
||||
<section id="community">
|
||||
<h2>Community extensions</h2>
|
||||
<p class="lede">Published by BCDN name holders, signed with the wallet that holds the name. Theseus installs them
|
||||
from <b>Settings › Extensions › Community</b> 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="note"><b>Trust, stated plainly.</b> A community extension runs with the same access as any add-on —
|
||||
treat one like an unsigned executable from that publisher. The signature proves <i>who</i> published it,
|
||||
not that it is safe.</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>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>
|
||||
// Fill each card from its signed manifest. Relative URLs keep this working on
|
||||
// every origin the page is served from (navigate.st relay, bns:// in Theseus).
|
||||
// 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 {
|
||||
|
|
@ -161,7 +186,28 @@
|
|||
set("count", (el) => { el.textContent = list.length + " signed version" + (list.length === 1 ? "" : "s") + " on the channel"; });
|
||||
} catch {}
|
||||
}
|
||||
function 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; }
|
||||
// Community catalog
|
||||
const box = document.getElementById("communityList");
|
||||
const esc = (s) => String(s || "").replace(/[&<>"']/g, (c) => ({ "&":"&","<":"<",">":">",'"':""","'":"'" })[c]);
|
||||
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; }
|
||||
box.innerHTML = list.map((e) => `
|
||||
<div class="ext" id="ext-${esc(e.id)}">
|
||||
<h3><span class="icon">${esc(e.icon || "🧩")}</span> ${esc(e.name)} <span class="cur" style="margin-left:auto">v${esc(e.latest)}</span></h3>
|
||||
<div class="desc">${esc(e.description)}</div>
|
||||
<div class="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="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>Install</b><code>Theseus › Settings › Extensions › Community › Install</code></div>
|
||||
</div>
|
||||
</div>`).join("");
|
||||
} catch (e) { box.innerHTML = '<div class="ext" style="color:var(--dim)">Catalog unavailable right now.</div>'; }
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
|
|
|||
7
extensions/js/bns-register.js
Normal file
7
extensions/js/bns-register.js
Normal file
File diff suppressed because one or more lines are too long
141
extensions/js/session.js
Normal file
141
extensions/js/session.js
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// Sirius.X persistent sign-in ("stay signed in on this device").
|
||||
//
|
||||
// Problem: every visit asked for the seed phrase, the password or the PIN
|
||||
// again. A dashboard people come back to daily cannot work like that.
|
||||
//
|
||||
// Model: after one successful sign-in the wallet's recovery phrase is
|
||||
// encrypted with a random AES-GCM key that lives ONLY inside IndexedDB as a
|
||||
// non-extractable CryptoKey (the browser can use it, scripts cannot read
|
||||
// its bytes). The ciphertext sits in localStorage. On the next visit the
|
||||
// dashboard decrypts silently and the user is in — no prompt. Signing out,
|
||||
// or turning "stay signed in" off in Settings, destroys both halves.
|
||||
//
|
||||
// Trust boundary, stated honestly: this is a device session, like a
|
||||
// "remember me" cookie. Anyone who can run scripts in this browser profile
|
||||
// can use the wallet while the session exists. The password-encrypted
|
||||
// wallet blob (bns.wallet.v1) and the optional PIN escrow are unchanged and
|
||||
// still the durable secrets. Users who want a prompt before every payment
|
||||
// can keep that in Settings ("Ask for PIN before payments").
|
||||
//
|
||||
// window.siriusSession = { isEnabled, setEnabled, remember, restore, forget,
|
||||
// requirePinForPayments, setRequirePinForPayments }
|
||||
|
||||
(() => {
|
||||
const DB = "sirius-session";
|
||||
const STORE = "keys";
|
||||
const KEY_ID = "device";
|
||||
const BLOB = "siriusSessionBlob";
|
||||
const ENABLED = "siriusStaySignedIn"; // "0" = off; anything else = on (default on)
|
||||
const REQUIRE_PIN = "siriusRequirePin"; // "1" = ask PIN before payments (default off)
|
||||
|
||||
const enc = new TextEncoder();
|
||||
const dec = new TextDecoder();
|
||||
const b64 = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes)));
|
||||
const unb64 = (str) => Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
|
||||
|
||||
function openDb() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!("indexedDB" in window)) return reject(new Error("IndexedDB unavailable"));
|
||||
const req = indexedDB.open(DB, 1);
|
||||
req.onupgradeneeded = () => { req.result.createObjectStore(STORE); };
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error || new Error("IndexedDB open failed"));
|
||||
});
|
||||
}
|
||||
function idbGet(db, key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, "readonly");
|
||||
const req = tx.objectStore(STORE).get(key);
|
||||
req.onsuccess = () => resolve(req.result || null);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
function idbPut(db, key, value) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, "readwrite");
|
||||
tx.objectStore(STORE).put(value, key);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
function idbDel(db, key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, "readwrite");
|
||||
tx.objectStore(STORE).delete(key);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
async function deviceKey(create) {
|
||||
const db = await openDb();
|
||||
try {
|
||||
let key = await idbGet(db, KEY_ID);
|
||||
if (!key && create) {
|
||||
key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
||||
await idbPut(db, KEY_ID, key);
|
||||
}
|
||||
return key;
|
||||
} finally { db.close(); }
|
||||
}
|
||||
|
||||
function isEnabled() {
|
||||
try { return localStorage.getItem(ENABLED) !== "0"; } catch { return true; }
|
||||
}
|
||||
function setEnabled(on) {
|
||||
try { localStorage.setItem(ENABLED, on ? "1" : "0"); } catch {}
|
||||
if (!on) forget();
|
||||
}
|
||||
function requirePinForPayments() {
|
||||
try { return localStorage.getItem(REQUIRE_PIN) === "1"; } catch { return false; }
|
||||
}
|
||||
function setRequirePinForPayments(on) {
|
||||
try { localStorage.setItem(REQUIRE_PIN, on ? "1" : "0"); } catch {}
|
||||
}
|
||||
function hasSession() {
|
||||
try { return !!localStorage.getItem(BLOB); } catch { return false; }
|
||||
}
|
||||
|
||||
// Encrypt and store the recovery phrase for silent restore. `meta` may
|
||||
// carry { accountPath } for wallets derived on a non-default path.
|
||||
async function remember(mnemonic, meta = {}) {
|
||||
if (!isEnabled() || !mnemonic) return false;
|
||||
try {
|
||||
const key = await deviceKey(true);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, enc.encode(mnemonic));
|
||||
localStorage.setItem(BLOB, JSON.stringify({ v: 1, iv: b64(iv), ct: b64(ct), at: Date.now(), path: meta.accountPath || null }));
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
// Silent restore. Resolves { mnemonic, accountPath } or null. Never throws.
|
||||
async function restore() {
|
||||
if (!isEnabled()) return null;
|
||||
let raw = null;
|
||||
try { raw = localStorage.getItem(BLOB); } catch {}
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const blob = JSON.parse(raw);
|
||||
const key = await deviceKey(false);
|
||||
if (!key) { forget(); return null; }
|
||||
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: unb64(blob.iv) }, key, unb64(blob.ct));
|
||||
return { mnemonic: dec.decode(pt), accountPath: blob.path || null };
|
||||
} catch {
|
||||
// Key rotated / storage cleared / tampered — drop the stale blob.
|
||||
forget();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function forget() {
|
||||
try { localStorage.removeItem(BLOB); } catch {}
|
||||
try { const db = await openDb(); try { await idbDel(db, KEY_ID); } finally { db.close(); } } catch {}
|
||||
}
|
||||
|
||||
window.siriusSession = {
|
||||
isEnabled, setEnabled, hasSession,
|
||||
remember, restore, forget,
|
||||
requirePinForPayments, setRequirePinForPayments,
|
||||
};
|
||||
})();
|
||||
239
extensions/publish/index.html
Normal file
239
extensions/publish/index.html
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>theseus.x — Publish an extension</title>
|
||||
<meta name="description" content="Publish a Theseus extension: sign the package with the wallet that holds your BCDN name; it is stored on Sia and listed in the catalog.">
|
||||
<meta name="robots" content="noindex">
|
||||
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
|
||||
<style>
|
||||
:root{--bg:#0b0e14;--panel:#141a24;--panel2:#18202c;--line:rgba(255,255,255,.09);--ink:#e7eaf1;--mut:#8b98a9;--dim:#5e6678;--acid:#d6ff3d;--ok:#4fd1a5;--bad:#f6768a}
|
||||
*{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:760px;margin:0 auto;padding:0 1.2rem 3rem}
|
||||
.topnav{position:sticky;top:0;z-index:20;display:flex;gap:2px;align-items:center;flex-wrap:wrap;padding:10px 1.2rem;background:rgba(11,14,20,.82);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
|
||||
.topnav .brand{font-weight:600;color:var(--ink);text-decoration:none;margin-right:14px;font-size:15px;display:flex;align-items:center;gap:8px}
|
||||
.topnav .brand .g{color:var(--acid)}
|
||||
.topnav a{color:var(--mut);text-decoration:none;padding:6px 12px;border-radius:8px;font-size:14px}
|
||||
.topnav a:hover{color:var(--ink);background:var(--panel)}
|
||||
.topnav a.here{color:var(--acid);background:rgba(214,255,61,.08)}
|
||||
h1{font-size:clamp(1.6rem,4vw,2.1rem);margin:2.2rem 0 .3rem}
|
||||
h1 .g{color:var(--acid)}
|
||||
.lede{color:var(--mut);margin:0 0 1.4rem}
|
||||
.step{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px 20px;margin:1rem 0}
|
||||
.step h2{font-size:1.05rem;margin:0 0 .6rem;display:flex;align-items:center;gap:10px}
|
||||
.step h2 .n{display:inline-grid;place-items:center;width:24px;height:24px;border-radius:50%;background:var(--acid);color:#0b0e14;font-size:13px;font-weight:700}
|
||||
.step h2 .done{color:var(--ok);font-size:13px;font-weight:500;margin-left:auto}
|
||||
label{display:block;color:var(--mut);font-size:13px;margin:.6rem 0 .25rem}
|
||||
input[type=text],textarea{width:100%;background:#0e131b;border:1px solid var(--line);border-radius:9px;color:var(--ink);padding:9px 11px;font:14px system-ui,sans-serif}
|
||||
textarea{font-family:ui-monospace,monospace;font-size:13px;min-height:74px}
|
||||
input[type=file]{color:var(--mut);font-size:13px}
|
||||
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:.6rem}
|
||||
.btn{display:inline-block;padding:.5rem 1rem;border-radius:9px;background:var(--acid);color:#0b0e14;font-weight:650;border:0;cursor:pointer;font:600 14px system-ui,sans-serif}
|
||||
.btn.ghost{background:transparent;color:var(--ink);border:1px solid var(--line)}
|
||||
.btn:disabled{opacity:.45;cursor:default}
|
||||
.st{font-size:13px;color:var(--mut)}
|
||||
.st.ok{color:var(--ok)} .st.bad{color:var(--bad)}
|
||||
code,.mono{background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-family:ui-monospace,monospace;font-size:12.5px;word-break:break-all}
|
||||
.kv{display:grid;grid-template-columns:110px 1fr;gap:4px 12px;font-size:13px;margin-top:.6rem}
|
||||
.kv b{color:var(--mut);font-weight:600}
|
||||
.note{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--acid);border-radius:10px;padding:12px 16px;color:var(--mut);font-size:13.5px;margin:1.2rem 0}
|
||||
.note b{color:var(--ink)}
|
||||
.chk{display:flex;gap:8px;align-items:center;color:var(--mut);font-size:13px;margin-top:.6rem}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="topnav">
|
||||
<a class="brand" href="/"><span>theseus<span class="g">.x</span></span></a>
|
||||
<a href="/">Home</a>
|
||||
<a href="/plug-ins/">Plug-ins</a>
|
||||
<a href="../">Extensions</a>
|
||||
<a class="here" href="./">Publish</a>
|
||||
</nav>
|
||||
<div class="wrap">
|
||||
<h1>Publish an <span class="g">extension</span></h1>
|
||||
<p class="lede">Three steps, all in this tab: unlock the wallet that holds your BCDN name, choose the packaged
|
||||
extension, sign. The gateway checks the signature against the name's on-chain owner, stores the package on
|
||||
Sia and updates the <a href="../#community">catalog</a>. Your recovery phrase never leaves this page.</p>
|
||||
|
||||
<div class="step" id="s1">
|
||||
<h2><span class="n">1</span> Wallet <span class="done" id="s1done" hidden>✓ unlocked</span></h2>
|
||||
<div id="walletForm">
|
||||
<label for="phrase">Recovery phrase of the wallet that holds your name (12 or 24 words)</label>
|
||||
<textarea id="phrase" spellcheck="false" autocomplete="off" placeholder="word word word …"></textarea>
|
||||
<label class="chk"><input type="checkbox" id="stay" checked> Stay signed in on this device (encrypted with a key only this browser profile can use)</label>
|
||||
<div class="row"><button class="btn" id="unlock">Unlock</button><span class="st" id="walletSt"></span></div>
|
||||
</div>
|
||||
<div id="walletInfo" hidden>
|
||||
<div class="kv"><b>Address</b><span class="mono" id="addr"></span></div>
|
||||
<div class="row"><button class="btn ghost" id="signout">Sign out</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step" id="s2">
|
||||
<h2><span class="n">2</span> Publisher name <span class="done" id="s2done" hidden>✓ you own it</span></h2>
|
||||
<label for="pname">A BCDN name you own (e.g. <code>you.bch</code>) — it becomes the publisher of record</label>
|
||||
<div class="row"><input type="text" id="pname" placeholder="name.tld" style="max-width:320px" spellcheck="false"><button class="btn ghost" id="checkName">Check ownership</button></div>
|
||||
<div class="st" id="nameSt"></div>
|
||||
</div>
|
||||
|
||||
<div class="step" id="s3">
|
||||
<h2><span class="n">3</span> Package <span class="done" id="s3done" hidden>✓ looks valid</span></h2>
|
||||
<label>Gzipped tar of the extension folder (<code>tar -czf my-ext-1.0.0.tar.gz -C my-ext .</code>, max 8 MB)</label>
|
||||
<input type="file" id="pkg" accept=".tar.gz,.tgz,application/gzip">
|
||||
<div class="kv" id="pkgInfo" hidden>
|
||||
<b>id</b><span id="pid"></span>
|
||||
<b>name</b><span id="pnameOut"></span>
|
||||
<b>version</b><span id="pver"></span>
|
||||
<b>main</b><span id="pmain"></span>
|
||||
<b>files</b><span id="pfiles"></span>
|
||||
<b>sha256</b><span class="mono" id="psha"></span>
|
||||
</div>
|
||||
<div class="st" id="pkgSt"></div>
|
||||
</div>
|
||||
|
||||
<div class="step" id="s4">
|
||||
<h2><span class="n">4</span> Sign and publish</h2>
|
||||
<p class="st">Two signatures are made with your wallet key: one authorises this upload, the other is stored in the
|
||||
extension's channel so every Theseus can verify the package came from your name.</p>
|
||||
<div class="row"><button class="btn" id="publish" disabled>Sign & publish</button><span class="st" id="pubSt"></span></div>
|
||||
<div id="result" hidden>
|
||||
<div class="kv">
|
||||
<b>Catalog</b><span><a href="../#community">theseus.x/extensions</a></span>
|
||||
<b>Channel</b><span class="mono" id="rUpdates"></span>
|
||||
<b>Package</b><span class="mono" id="rTarball"></span>
|
||||
</div>
|
||||
<p class="st" style="margin-top:.6rem">Install it from Theseus › Settings › Extensions › Community. New versions: come back with a higher version number.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note"><b>What the gateway enforces.</b> The signature must recover to the name's current NFT owner ·
|
||||
<code>addon.json</code> at the package root with matching <code>id</code>/<code>version</code> and a <code>main</code> script ·
|
||||
an id belongs to the first name that publishes it · each new version must be higher than the last · 8 MB cap.</div>
|
||||
</div>
|
||||
|
||||
<script src="../js/session.js"></script>
|
||||
<script type="module">
|
||||
import * as BNS from "../js/bns-register.js";
|
||||
const API = "https://silentmode.st";
|
||||
const NAME_API = "https://navigate.st";
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const st = (id, text, cls = "") => { const el = $(id); el.textContent = text; el.className = "st " + cls; };
|
||||
const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
const b64 = (u8) => btoa(String.fromCharCode(...u8));
|
||||
let wallet = null, pubName = "", pkgBytes = null, pkgSha = "", manifest = null;
|
||||
|
||||
function showWallet() {
|
||||
$("walletForm").hidden = !!wallet; $("walletInfo").hidden = !wallet; $("s1done").hidden = !wallet;
|
||||
if (wallet) $("addr").textContent = wallet.address;
|
||||
gate();
|
||||
}
|
||||
function gate() { $("publish").disabled = !(wallet && pubName && pkgBytes && manifest); }
|
||||
|
||||
// --- 1. wallet ---
|
||||
(async () => {
|
||||
try {
|
||||
const S = window.siriusSession;
|
||||
const s = S?.restore ? await S.restore() : null;
|
||||
if (s?.mnemonic) { wallet = await BNS.BuiltInWallet.fromMnemonic(s.mnemonic, BNS.CHIPNET_PREFIX, s.accountPath || undefined); showWallet(); }
|
||||
} catch {}
|
||||
})();
|
||||
$("unlock").onclick = async () => {
|
||||
const phrase = $("phrase").value.trim().toLowerCase().replace(/\s+/g, " ");
|
||||
const n = phrase.split(" ").length;
|
||||
if (n !== 12 && n !== 24) return st("walletSt", "A recovery phrase has 12 or 24 words.", "bad");
|
||||
st("walletSt", "Deriving…");
|
||||
try {
|
||||
wallet = await BNS.BuiltInWallet.fromMnemonic(phrase, BNS.CHIPNET_PREFIX);
|
||||
if ($("stay").checked && window.siriusSession?.remember) await window.siriusSession.remember(phrase);
|
||||
$("phrase").value = "";
|
||||
st("walletSt", ""); showWallet();
|
||||
} catch (e) { st("walletSt", "Could not derive a wallet: " + (e?.message || e), "bad"); }
|
||||
};
|
||||
$("signout").onclick = async () => { wallet = null; try { await window.siriusSession?.forget(); } catch {} showWallet(); };
|
||||
|
||||
// --- 2. name ownership ---
|
||||
$("checkName").onclick = async () => {
|
||||
const name = $("pname").value.trim().toLowerCase();
|
||||
pubName = ""; $("s2done").hidden = true; gate();
|
||||
if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(name)) return st("nameSt", "Enter a registered name like you.bch.", "bad");
|
||||
if (!wallet) return st("nameSt", "Unlock the wallet first.", "bad");
|
||||
st("nameSt", "Looking up the owner…");
|
||||
try {
|
||||
const r = await fetch(`${NAME_API}/api/name/${encodeURIComponent(name)}`, { cache: "no-store" });
|
||||
const j = await r.json();
|
||||
if (!j.registered) return st("nameSt", `${name} is not registered.`, "bad");
|
||||
if (!j.owner) return st("nameSt", "The index has no owner for this name yet — try again in a minute.", "bad");
|
||||
if (j.owner !== wallet.address) return st("nameSt", `${name} is owned by ${j.owner}, not by this wallet.`, "bad");
|
||||
pubName = name; $("s2done").hidden = false; st("nameSt", "This wallet holds " + name + ".", "ok"); gate();
|
||||
} catch (e) { st("nameSt", "Lookup failed: " + (e?.message || e), "bad"); }
|
||||
};
|
||||
|
||||
// --- 3. package: sha256 + read addon.json out of the gzipped tar ---
|
||||
async function gunzip(bytes) {
|
||||
const ds = new DecompressionStream("gzip");
|
||||
const out = new Blob([bytes]).stream().pipeThrough(ds);
|
||||
return new Uint8Array(await new Response(out).arrayBuffer());
|
||||
}
|
||||
function tarFiles(raw) {
|
||||
const files = []; const td = new TextDecoder();
|
||||
const cstr = (a, b) => td.decode(raw.subarray(a, b)).replace(/\0[\s\S]*$/, "");
|
||||
for (let off = 0; off + 512 <= raw.length; ) {
|
||||
const hdr = raw.subarray(off, off + 512);
|
||||
if (hdr.every((x) => x === 0)) break;
|
||||
let name = cstr(off, off + 100);
|
||||
const size = parseInt(cstr(off + 124, off + 136).trim() || "0", 8) || 0;
|
||||
const type = String.fromCharCode(hdr[156] || 48);
|
||||
if (cstr(off + 257, off + 263) === "ustar") { const pre = cstr(off + 345, off + 500); if (pre) name = pre + "/" + name; }
|
||||
name = name.replace(/^\.\//, "");
|
||||
if (type === "0" || type === "\0") files.push({ name, data: raw.subarray(off + 512, off + 512 + size) });
|
||||
off += 512 + Math.ceil(size / 512) * 512;
|
||||
}
|
||||
return files;
|
||||
}
|
||||
$("pkg").onchange = async () => {
|
||||
pkgBytes = null; manifest = null; $("pkgInfo").hidden = true; $("s3done").hidden = true; gate();
|
||||
const f = $("pkg").files[0]; if (!f) return;
|
||||
if (f.size > 8 * 1024 * 1024) return st("pkgSt", "Package is over 8 MB.", "bad");
|
||||
try {
|
||||
const bytes = new Uint8Array(await f.arrayBuffer());
|
||||
const files = tarFiles(await gunzip(bytes));
|
||||
const mf = files.filter((x) => x.name === "addon.json" || /^[^/]+\/addon\.json$/.test(x.name)).sort((a, b) => a.name.length - b.name.length)[0];
|
||||
if (!mf) return st("pkgSt", "No addon.json at the package root.", "bad");
|
||||
const root = mf.name === "addon.json" ? "" : mf.name.slice(0, -"addon.json".length);
|
||||
const m = JSON.parse(new TextDecoder().decode(mf.data));
|
||||
if (!/^[a-z0-9][a-z0-9._-]{1,63}$/.test(m.id || "")) return st("pkgSt", "addon.json id must be 2–64 chars of [a-z0-9._-].", "bad");
|
||||
if (!/^\d+(\.\d+){1,3}$/.test(m.version || "")) return st("pkgSt", "addon.json version must look like 1.2.3.", "bad");
|
||||
if (!m.main || !files.some((x) => x.name === root + String(m.main).replace(/^\.\//, ""))) return st("pkgSt", `main script "${m.main}" is not in the package.`, "bad");
|
||||
if (!m.name) return st("pkgSt", "addon.json needs a name.", "bad");
|
||||
pkgBytes = bytes; manifest = m; pkgSha = hex(await crypto.subtle.digest("SHA-256", bytes));
|
||||
$("pid").textContent = m.id; $("pnameOut").textContent = m.name; $("pver").textContent = m.version; $("pmain").textContent = m.main;
|
||||
$("pfiles").textContent = files.length + " files, " + (bytes.length / 1024).toFixed(1) + " KB"; $("psha").textContent = pkgSha;
|
||||
$("pkgInfo").hidden = false; $("s3done").hidden = false; st("pkgSt", ""); gate();
|
||||
} catch (e) { st("pkgSt", "Could not read the package: " + (e?.message || e), "bad"); }
|
||||
};
|
||||
|
||||
// --- 4. sign + PUT ---
|
||||
$("publish").onclick = async () => {
|
||||
if (!(wallet && pubName && pkgBytes && manifest)) return;
|
||||
$("publish").disabled = true; st("pubSt", "Signing…");
|
||||
try {
|
||||
const ts = String(Date.now());
|
||||
const digest = async (msg) => new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg)));
|
||||
const sig = b64(wallet.signMessage(await digest(`BNS-EXT1\n${pubName}\n${manifest.id}\n${manifest.version}\n${pkgSha}\n${ts}`)));
|
||||
const entrySig = b64(wallet.signMessage(await digest(`silentmode.extension-v1|${manifest.id}|${manifest.version}|${pkgSha}|${pubName}`)));
|
||||
st("pubSt", "Uploading to the gateway…");
|
||||
const r = await fetch(`${API}/api/ext/${encodeURIComponent(pubName)}/${manifest.id}/${manifest.version}`, {
|
||||
method: "PUT", body: pkgBytes, headers: { "content-type": "application/gzip", "x-bns-ts": ts, "x-bns-sig": sig, "x-bns-entry-sig": entrySig },
|
||||
});
|
||||
const j = await r.json().catch(() => ({}));
|
||||
if (!r.ok) throw new Error(j.error || ("HTTP " + r.status));
|
||||
st("pubSt", `Published ${j.id} v${j.version} as ${j.publisher}.`, "ok");
|
||||
$("rUpdates").textContent = j.updates; $("rTarball").textContent = j.tarball; $("result").hidden = false;
|
||||
} catch (e) { st("pubSt", "Publish failed: " + (e?.message || e), "bad"); $("publish").disabled = false; }
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -103,6 +103,7 @@
|
|||
<a href="#features">Features</a>
|
||||
<a href="#download">Download</a>
|
||||
<a href="#verify">Verify</a>
|
||||
<a href="/plug-ins/">Plug-ins</a>
|
||||
<a href="/extensions/">Extensions</a>
|
||||
<a href="https://sirius.x/" title="Register a BCNR name">sirius.x</a>
|
||||
<a class="get" href="#download">Get Theseus</a>
|
||||
|
|
|
|||
177
plug-ins/index.html
Normal file
177
plug-ins/index.html
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>theseus.x — Plug-ins</title>
|
||||
<meta name="description" content="Silent Mode's first-party plug-ins for Theseus Navigator: the Aegis wallet and the Ariadne's Thread system-wide resolver. Current signed versions, hashes, and how they update.">
|
||||
<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 — Plug-ins">
|
||||
<meta property="og:description" content="First-party plug-ins for Theseus: Aegis wallet and Ariadne's Thread. Signed, verified on the device.">
|
||||
<meta property="og:url" content="https://theseus.x/plug-ins/">
|
||||
<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:sticky;top:0;z-index:20;display:flex;gap:2px;align-items:center;flex-wrap:wrap;
|
||||
padding:10px 1.2rem;background:rgba(11,14,20,.82);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
|
||||
.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}
|
||||
.topnav a:hover{color:var(--ink);background:var(--panel)}
|
||||
.topnav a.here{color:var(--acid);background:rgba(214,255,61,.08)}
|
||||
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}
|
||||
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{background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-family:ui-monospace,monospace;font-size:13px}
|
||||
.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;flex-wrap:wrap}
|
||||
.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:90px}
|
||||
.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}
|
||||
.btns{display:flex;gap:8px;flex-wrap:wrap;margin-top:.8rem}
|
||||
.btn{display:inline-block;padding:.45rem .9rem;border-radius:9px;background:var(--acid);color:#0b0e14;font-weight:650;text-decoration:none;font-size:14px}
|
||||
.btn.ghost{background:transparent;color:var(--ink);border:1px solid var(--line)}
|
||||
.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)}
|
||||
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="/">Home</a>
|
||||
<a href="/#download">Download</a>
|
||||
<a class="here" href="./">Plug-ins</a>
|
||||
<a href="/extensions/">Extensions</a>
|
||||
<a href="https://sirius.x/">sirius.x</a>
|
||||
</nav>
|
||||
|
||||
<header class="hero">
|
||||
<h1>Theseus <span class="g">Plug-ins</span></h1>
|
||||
<p class="tag">Silent Mode's own components that ship inside Theseus and appear under
|
||||
<b>Settings › Plug-ins</b>: the wallet and the system-wide resolver. Community-made
|
||||
add-ons live on the separate <a href="/extensions/">Extensions</a> page.</p>
|
||||
</header>
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<section>
|
||||
<h2>Aegis — the wallet</h2>
|
||||
<p class="lede">Multi-chain wallet derived from your Theseus vault (BCH, BTC, TRX, ETH, SOL, SC, DGB) with a
|
||||
dApp bridge for the pages you visit. Bundled with every Theseus build and updated over a signed channel
|
||||
between releases; the panel's own <i>Stage → Restart</i> flow applies an update without reinstalling Theseus.</p>
|
||||
<div class="ext" id="aegis" data-channel="../extensions/aegis/updates.json">
|
||||
<h3><span class="icon">🛡️</span> Aegis <span class="cur" style="margin-left:auto">current <span data-live="version">0.6.31</span></span></h3>
|
||||
<div class="desc">Vault-derived keys, per-chain accounts, imported keys, message signing, dApp permissions per site.</div>
|
||||
<div class="meta">Plug-in · Settings › Plug-ins · <span data-live="count">signed versions on the channel</span></div>
|
||||
<div class="verifybox">
|
||||
<div class="lbl">Signed update manifest — Ed25519 by the Silent Mode operator, SHA-256 verified before install</div>
|
||||
<div class="h"><b>updates.json</b><code><a href="../extensions/aegis/updates.json">theseus.x/extensions/aegis/updates.json</a></code></div>
|
||||
<div class="h"><b>Tarball</b><code><a data-live="url" href="../extensions/aegis/">…</a></code></div>
|
||||
<div class="h"><b>sha256</b><code data-live="sha256">—</code></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Ariadne's Thread — the system-wide resolver</h2>
|
||||
<p class="lede">Makes BCDN names resolve for <em>every</em> program on the machine, not only Theseus: a local
|
||||
DNS resolver plus gateway, installed by Theseus's setup by default (opt-out) and switchable from
|
||||
<b>Settings › Plug-ins</b>. The same resolver ships standalone for Windows and as Ariadne for Android.</p>
|
||||
<div class="ext" id="ariadne">
|
||||
<h3><span class="icon">🧵</span> Ariadne's Thread <span class="cur" style="margin-left:auto">current <span data-live="resolver-version">0.1.3</span></span></h3>
|
||||
<div class="desc">Windows service: answers A/AAAA for registered names, forwards everything else to your upstream
|
||||
resolver, serves Sia-hosted content through a local gateway with a machine-local CA. Answers MX/TXT/NS from
|
||||
owner-signed DNS records.</div>
|
||||
<div class="meta">Plug-in · Settings › Plug-ins · Turn on / off · Install / Update / Uninstall from inside Theseus</div>
|
||||
<div class="btns">
|
||||
<a class="btn" href="https://dl.silentmode.st/latest/AriadneResolver-Setup.exe">Windows installer</a>
|
||||
<a class="btn ghost" href="https://dl.silentmode.st/latest/ariadne.apk">Ariadne for Android (APK) · v<span data-live="android-version">0.19</span></a>
|
||||
</div>
|
||||
<div class="verifybox">
|
||||
<div class="lbl">Hashes from the releases manifest — verify before running</div>
|
||||
<div class="h"><b>Installer</b><code data-live="resolver-sha">—</code></div>
|
||||
<div class="h"><b>APK</b><code data-live="android-sha">—</code></div>
|
||||
<div class="h"><b>Manifest</b><code><a href="https://dl.silentmode.st/releases-manifest.json">dl.silentmode.st/releases-manifest.json</a></code></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Where the line is</h2>
|
||||
<p class="lede">Plug-ins are first-party: Silent Mode builds them, signs them with the operator key, and Theseus treats
|
||||
them as part of itself (own Settings section, own update flow). Extensions are everyone else's: published by
|
||||
any BCDN name holder, signed with that name's wallet, listed on <a href="/extensions/">theseus.x/extensions</a>
|
||||
and installed from <b>Settings › Extensions</b>. Both are verified on your device before a byte runs.</p>
|
||||
<div class="note"><b>Want to build one?</b> Start on the <a href="/extensions/#build">Extensions page</a> — the same add-on
|
||||
format powers both, and publishing takes a name and a signature, nothing else.</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Silent Mode · <a href="https://code.silentmode.st/silentmode/theseus/src/branch/master/TheseusNavigator/docs/ADDON-UPDATES.md">how signed updates work</a>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(async () => {
|
||||
const set = (k, f) => document.querySelectorAll('[data-live="' + k + '"]').forEach(f);
|
||||
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; };
|
||||
try {
|
||||
const r = await fetch("../extensions/aegis/updates.json", { cache: "no-store" });
|
||||
if (r.ok) {
|
||||
const m = await r.json(); const list = (m.addons || []).slice().sort((x, y) => cmp(y.version, x.version)); const top = list[0];
|
||||
if (top) {
|
||||
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("count", (el) => { el.textContent = list.length + " signed version" + (list.length === 1 ? "" : "s") + " on the channel"; });
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
const r = await fetch("https://dl.silentmode.st/releases-manifest.json", { cache: "no-store" });
|
||||
if (r.ok) {
|
||||
const m = await r.json(); const g = (id) => (m.releases || []).find((x) => x.id === id);
|
||||
const res = g("ariadne-resolver"), an = g("ariadne-android");
|
||||
if (res) { set("resolver-version", (el) => { el.textContent = res.version; }); const f = Object.keys(res.files || {})[0]; set("resolver-sha", (el) => { el.textContent = f ? res.files[f] : "—"; }); }
|
||||
if (an) { set("android-version", (el) => { el.textContent = an.version; }); const f = Object.keys(an.files || {})[0]; set("android-sha", (el) => { el.textContent = f ? an.files[f] : "—"; }); }
|
||||
}
|
||||
} catch {}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Reference in a new issue