Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
// Theseus add-on framework — loader + API surface.
|
|
|
|
|
|
//
|
2026-09-21 01:55:25 +02:00
|
|
|
|
// Add-ons live in <userData>/extensions/<id>/ as ordinary folders on disk. Each
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
// carries an `addon.json` manifest and (per the manifest's `main` field) a
|
|
|
|
|
|
// CommonJS entry that exports `activate(api)` and optionally `deactivate()`.
|
|
|
|
|
|
// Nothing about an add-on ships in the Theseus repo or installer — drop a
|
|
|
|
|
|
// folder, restart Theseus, it's live. This is the same trust model as
|
|
|
|
|
|
// dev-mode browser extensions: the user is choosing to run local code with
|
|
|
|
|
|
// the app's full privileges.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Loading is synchronous at app-ready time; there is no hot-reload. Failed
|
|
|
|
|
|
// activations are logged and skipped without breaking the app.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Persistence:
|
|
|
|
|
|
// settings.disabledAddons — ids the user has toggled off
|
2026-09-21 01:55:25 +02:00
|
|
|
|
// <userData>/extensions-data/<id>.json — per-add-on kv store (api.storage)
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
|
|
|
|
|
|
const fs = require("node:fs");
|
|
|
|
|
|
const path = require("node:path");
|
|
|
|
|
|
|
|
|
|
|
|
// Extension points the framework understands. Extending this list means also
|
|
|
|
|
|
// teaching main.js and (typically) the chrome renderer about the new point.
|
|
|
|
|
|
// Right now only sidebar panels are wired — future rev adds toolbar-chip,
|
|
|
|
|
|
// proxy, page-inject, etc.
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
const KNOWN_CAPABILITIES = new Set([
|
|
|
|
|
|
"sidebar-panel", "session-proxy",
|
|
|
|
|
|
// vault-derive: api.vault.derive(purposePath) — HKDF child of the password
|
|
|
|
|
|
// vault's root, namespaced under the add-on id.
|
|
|
|
|
|
// page-inject: manifest["page-inject"] = { preload, origins } — the add-on's
|
|
|
|
|
|
// preload source runs in the isolated world of every tab whose
|
|
|
|
|
|
// URL matches one of the origin patterns.
|
|
|
|
|
|
// approval-modal: api.approvalModal({...}) — user-facing consent dialog over
|
|
|
|
|
|
// the active tab, resolved by main.
|
2026-09-07 00:36:08 +02:00
|
|
|
|
// capture-tab: api.captureTab({mode, ...}) + api.saveCapture({dataUrl, filename})
|
|
|
|
|
|
// — snapshot the active tab (visible viewport / full page /
|
|
|
|
|
|
// user-drawn rectangle) and save the result through the app's
|
|
|
|
|
|
// downloads pipeline. The add-on sees pixels of whatever the
|
|
|
|
|
|
// current tab is showing, so this is the same trust bar as a
|
|
|
|
|
|
// page-inject add-on that matches "*://*/*".
|
feat(theseus/screenshot): full-tab editor + toolbar-menu + open-tab capabilities
Reworks the screenshot addon into the flow the user asked for: the
dock icon opens a small dropdown menu (Visible viewport / Full page /
Region…) instead of the sidebar picker, and each capture opens a
full browser tab hosting an editor.
Two new addon-host capabilities land alongside:
- toolbar-menu: the addon declares an icon + item list in its manifest;
the chrome dock renders a button that, on click, opens a small menu
and dispatches the selection to the addon via addon-menu-select IPC.
- open-tab: api.openTab(path) opens a browser tab whose URL is the
addon's local file. Origin-gated per addon; the editor uses a
dedicated addon-tab-preload for its main → renderer bridge.
Editor page (editor.html/js/css):
- Crop, arrow, rectangle, circle, freehand pen, text, blur
- Colour swatches (red / yellow / acid / white / black), 3 stroke widths
- Undo/redo command stack, zoom controls
- Save PNG (goes through the download pipeline, chip picks it up)
- Copy to clipboard via ClipboardItem
2026-09-07 00:56:57 +02:00
|
|
|
|
// toolbar-menu: manifest["toolbar-menu"] = { title?, icon?, items:[{id,label,icon?}] }
|
|
|
|
|
|
// — chrome renders a dropdown under the add-on's dock icon;
|
|
|
|
|
|
// picking an item dispatches "menu-select" with {id} to the
|
|
|
|
|
|
// add-on's onMessage("menu-select", …) handler.
|
|
|
|
|
|
// open-tab: api.openTab(pathOrUrl, {query?}) — for a bare http(s) URL
|
|
|
|
|
|
// this stays available without the capability (legacy).
|
|
|
|
|
|
// Declaring "open-tab" additionally lets the add-on open
|
|
|
|
|
|
// one of its OWN HTML files as a full Theseus tab, with a
|
|
|
|
|
|
// lean preload so the page can keep talking to the add-on
|
|
|
|
|
|
// via window.silentmode.invoke().
|
2026-09-07 00:36:08 +02:00
|
|
|
|
"vault-derive", "page-inject", "approval-modal", "capture-tab",
|
feat(theseus/screenshot): full-tab editor + toolbar-menu + open-tab capabilities
Reworks the screenshot addon into the flow the user asked for: the
dock icon opens a small dropdown menu (Visible viewport / Full page /
Region…) instead of the sidebar picker, and each capture opens a
full browser tab hosting an editor.
Two new addon-host capabilities land alongside:
- toolbar-menu: the addon declares an icon + item list in its manifest;
the chrome dock renders a button that, on click, opens a small menu
and dispatches the selection to the addon via addon-menu-select IPC.
- open-tab: api.openTab(path) opens a browser tab whose URL is the
addon's local file. Origin-gated per addon; the editor uses a
dedicated addon-tab-preload for its main → renderer bridge.
Editor page (editor.html/js/css):
- Crop, arrow, rectangle, circle, freehand pen, text, blur
- Colour swatches (red / yellow / acid / white / black), 3 stroke widths
- Undo/redo command stack, zoom controls
- Save PNG (goes through the download pipeline, chip picks it up)
- Copy to clipboard via ClipboardItem
2026-09-07 00:56:57 +02:00
|
|
|
|
"toolbar-menu", "open-tab",
|
feat(theseus/addons): context-menu-item capability + api.revealSidebar
Adds a new "context-menu-item" capability. Add-ons declare a
"context-menu-items" array in their manifest:
{
"capabilities": ["context-menu-item", ...],
"context-menu-items": [
{ "id": "translate-selection", "label": "Translate selection",
"when": "selectionText", "icon": "🌐" }
]
}
The `when` filter is one of selectionText | linkURL | editable | image
| always. Right-click on a page, and items whose `when` matches the
current context get merged into the native menu after the built-in
Search-for entry, before Back/Forward/Reload. Both context-menu
handlers (main tab area + detached link windows) share the same
merging logic.
Picking an item dispatches "context-menu" to the add-on's onMessage
handler with the full context (selectionText, linkURL, mediaType,
srcURL, pageURL, host). The add-on decides what to do — the
translate add-on stashes the selection to storage and calls
api.revealSidebar("main") which surfaces its own sidebar panel.
api.revealSidebar(panelId) is the paired hook. Ownership is enforced
by the host — an add-on can only reveal panels it registered —
before routing to main's setSidebar path.
Unknown capabilities were already silently dropped by
validateManifest, so older Theseus builds that don't understand
"context-menu-item" just ignore it, and the manifest still loads.
Add-ons that also declare "sidebar-panel" keep working; the new
capability doesn't require it.
This is the wiring that pairs with the translate/ add-on landed in
4498fbb — right-click "Translate selection" is live once this ships.
2026-09-20 18:27:10 +02:00
|
|
|
|
// context-menu-item: manifest["context-menu-items"] = [{id, label, when, icon?}]
|
|
|
|
|
|
// — chrome merges these into every tab's right-click
|
|
|
|
|
|
// menu, filtered by `when` (selectionText | linkURL |
|
|
|
|
|
|
// editable | image | always). Picking one dispatches
|
|
|
|
|
|
// "context-menu" with the item id + the surrounding
|
|
|
|
|
|
// context (selection text, link URL, media info, host)
|
|
|
|
|
|
// to the add-on's onMessage handler. Add-ons that also
|
|
|
|
|
|
// declare "sidebar-panel" typically follow up with
|
|
|
|
|
|
// api.revealSidebar(panelId) to surface the result.
|
|
|
|
|
|
"context-menu-item",
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// Chrome-style match pattern → predicate. "<scheme>://<host>/<path>" where
|
|
|
|
|
|
// scheme may be "*", host may start with "*." (matches the bare host and any
|
|
|
|
|
|
// subdomain) or be "*", and path is a glob where "*" matches anything.
|
|
|
|
|
|
// bns:// (how Theseus fetches BCNR sites internally) is folded into https://
|
|
|
|
|
|
// so a pattern written the way the address bar shows it keeps working.
|
|
|
|
|
|
function compileOriginPattern(pattern) {
|
|
|
|
|
|
const m = /^(\*|[a-z][a-z0-9+.-]*):\/\/(\*|\*\.[^/*]+|[^/*]+)(\/.*)?$/i.exec(String(pattern).trim());
|
|
|
|
|
|
if (!m) throw new Error(`bad origin pattern: ${pattern}`);
|
|
|
|
|
|
const [, scheme, host, pathGlob = "/*"] = m;
|
|
|
|
|
|
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
|
|
|
|
const schemeRe = scheme === "*" ? "https?" : esc(scheme.toLowerCase());
|
|
|
|
|
|
let hostRe;
|
|
|
|
|
|
if (host === "*") hostRe = "[^/]+";
|
|
|
|
|
|
else if (host.startsWith("*.")) hostRe = `(?:[^/]+\\.)?${esc(host.slice(2).toLowerCase())}`;
|
|
|
|
|
|
else hostRe = esc(host.toLowerCase());
|
|
|
|
|
|
const pathRe = pathGlob.split("*").map(esc).join(".*");
|
|
|
|
|
|
const re = new RegExp(`^${schemeRe}://${hostRe}(?::\\d+)?${pathRe}$`, "i");
|
|
|
|
|
|
return (url) => re.test(String(url).replace(/^bns:\/\//i, "https://"));
|
|
|
|
|
|
}
|
|
|
|
|
|
function urlMatchesAny(url, matchers) {
|
|
|
|
|
|
for (const fn of matchers) { try { if (fn(url)) return true; } catch {} }
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
|
|
|
|
|
|
// Manifest field guardrails. Reject anything shape-suspicious so a bad
|
|
|
|
|
|
// addon.json can't get past the loader gate.
|
|
|
|
|
|
function validateManifest(raw, folderName) {
|
|
|
|
|
|
const m = raw && typeof raw === "object" ? raw : {};
|
|
|
|
|
|
const id = String(m.id || "").trim();
|
|
|
|
|
|
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(id)) {
|
|
|
|
|
|
throw new Error(`invalid or missing "id" (allowed: [a-z0-9._-], up to 64 chars) — folder ${folderName}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const name = String(m.name || id);
|
|
|
|
|
|
const version = String(m.version || "0.0.0");
|
|
|
|
|
|
const description = String(m.description || "");
|
|
|
|
|
|
const author = String(m.author || "");
|
|
|
|
|
|
const icon = String(m.icon || "🧩");
|
|
|
|
|
|
const main = String(m.main || "index.js");
|
|
|
|
|
|
if (main.includes("..") || path.isAbsolute(main)) {
|
|
|
|
|
|
throw new Error(`addon "${id}": main must be a relative path inside the addon folder`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const capabilities = Array.isArray(m.capabilities) ? m.capabilities.map(String) : [];
|
|
|
|
|
|
for (const cap of capabilities) {
|
|
|
|
|
|
if (!KNOWN_CAPABILITIES.has(cap)) {
|
|
|
|
|
|
// Not fatal — log later. Unknown caps are silently dropped.
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
let pageInject = null;
|
|
|
|
|
|
if (capabilities.includes("page-inject")) {
|
|
|
|
|
|
const pi = m["page-inject"];
|
|
|
|
|
|
if (!pi || typeof pi !== "object") throw new Error(`addon "${id}": "page-inject" capability needs a "page-inject" manifest block`);
|
|
|
|
|
|
const preload = String(pi.preload || "");
|
|
|
|
|
|
if (!preload || preload.includes("..") || path.isAbsolute(preload)) {
|
|
|
|
|
|
throw new Error(`addon "${id}": page-inject.preload must be a relative path inside the addon folder`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const origins = Array.isArray(pi.origins) ? pi.origins.map(String) : [];
|
|
|
|
|
|
if (!origins.length) throw new Error(`addon "${id}": page-inject.origins must list at least one pattern`);
|
|
|
|
|
|
pageInject = { preload, origins, matchers: origins.map(compileOriginPattern) };
|
|
|
|
|
|
}
|
feat(theseus/screenshot): full-tab editor + toolbar-menu + open-tab capabilities
Reworks the screenshot addon into the flow the user asked for: the
dock icon opens a small dropdown menu (Visible viewport / Full page /
Region…) instead of the sidebar picker, and each capture opens a
full browser tab hosting an editor.
Two new addon-host capabilities land alongside:
- toolbar-menu: the addon declares an icon + item list in its manifest;
the chrome dock renders a button that, on click, opens a small menu
and dispatches the selection to the addon via addon-menu-select IPC.
- open-tab: api.openTab(path) opens a browser tab whose URL is the
addon's local file. Origin-gated per addon; the editor uses a
dedicated addon-tab-preload for its main → renderer bridge.
Editor page (editor.html/js/css):
- Crop, arrow, rectangle, circle, freehand pen, text, blur
- Colour swatches (red / yellow / acid / white / black), 3 stroke widths
- Undo/redo command stack, zoom controls
- Save PNG (goes through the download pipeline, chip picks it up)
- Copy to clipboard via ClipboardItem
2026-09-07 00:56:57 +02:00
|
|
|
|
let toolbarMenu = null;
|
|
|
|
|
|
if (capabilities.includes("toolbar-menu")) {
|
|
|
|
|
|
const tm = m["toolbar-menu"];
|
|
|
|
|
|
if (!tm || typeof tm !== "object") {
|
|
|
|
|
|
throw new Error(`addon "${id}": "toolbar-menu" capability needs a "toolbar-menu" manifest block`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const items = Array.isArray(tm.items) ? tm.items : [];
|
|
|
|
|
|
if (!items.length) throw new Error(`addon "${id}": toolbar-menu.items must list at least one entry`);
|
|
|
|
|
|
const seen = new Set();
|
|
|
|
|
|
const cleanItems = items.map((it, idx) => {
|
|
|
|
|
|
const iid = String(it && it.id || "").trim();
|
|
|
|
|
|
if (!iid || !/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(iid)) {
|
|
|
|
|
|
throw new Error(`addon "${id}": toolbar-menu.items[${idx}].id is required and must match [a-z0-9._-]`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (seen.has(iid)) throw new Error(`addon "${id}": toolbar-menu.items[${idx}].id "${iid}" duplicates an earlier entry`);
|
|
|
|
|
|
seen.add(iid);
|
|
|
|
|
|
const label = String(it.label || iid);
|
|
|
|
|
|
const itemIcon = it.icon == null ? "" : String(it.icon);
|
|
|
|
|
|
return { id: iid, label, icon: itemIcon };
|
|
|
|
|
|
});
|
|
|
|
|
|
toolbarMenu = {
|
|
|
|
|
|
title: tm.title == null ? name : String(tm.title),
|
|
|
|
|
|
icon: tm.icon == null ? icon : String(tm.icon),
|
|
|
|
|
|
items: cleanItems,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
feat(theseus/addons): context-menu-item capability + api.revealSidebar
Adds a new "context-menu-item" capability. Add-ons declare a
"context-menu-items" array in their manifest:
{
"capabilities": ["context-menu-item", ...],
"context-menu-items": [
{ "id": "translate-selection", "label": "Translate selection",
"when": "selectionText", "icon": "🌐" }
]
}
The `when` filter is one of selectionText | linkURL | editable | image
| always. Right-click on a page, and items whose `when` matches the
current context get merged into the native menu after the built-in
Search-for entry, before Back/Forward/Reload. Both context-menu
handlers (main tab area + detached link windows) share the same
merging logic.
Picking an item dispatches "context-menu" to the add-on's onMessage
handler with the full context (selectionText, linkURL, mediaType,
srcURL, pageURL, host). The add-on decides what to do — the
translate add-on stashes the selection to storage and calls
api.revealSidebar("main") which surfaces its own sidebar panel.
api.revealSidebar(panelId) is the paired hook. Ownership is enforced
by the host — an add-on can only reveal panels it registered —
before routing to main's setSidebar path.
Unknown capabilities were already silently dropped by
validateManifest, so older Theseus builds that don't understand
"context-menu-item" just ignore it, and the manifest still loads.
Add-ons that also declare "sidebar-panel" keep working; the new
capability doesn't require it.
This is the wiring that pairs with the translate/ add-on landed in
4498fbb — right-click "Translate selection" is live once this ships.
2026-09-20 18:27:10 +02:00
|
|
|
|
const contextMenuItems = [];
|
|
|
|
|
|
if (capabilities.includes("context-menu-item")) {
|
|
|
|
|
|
const rawItems = m["context-menu-items"];
|
|
|
|
|
|
if (!Array.isArray(rawItems) || !rawItems.length) {
|
|
|
|
|
|
throw new Error(`addon "${id}": "context-menu-item" capability needs a "context-menu-items" array with at least one entry`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const seen = new Set();
|
|
|
|
|
|
const ALLOWED_WHEN = new Set(["selectionText", "linkURL", "editable", "image", "always"]);
|
|
|
|
|
|
for (let idx = 0; idx < rawItems.length; idx++) {
|
|
|
|
|
|
const it = rawItems[idx];
|
|
|
|
|
|
const iid = String(it && it.id || "").trim();
|
|
|
|
|
|
if (!iid || !/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(iid)) {
|
|
|
|
|
|
throw new Error(`addon "${id}": context-menu-items[${idx}].id is required and must match [a-z0-9._-]`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (seen.has(iid)) throw new Error(`addon "${id}": context-menu-items[${idx}].id "${iid}" duplicates an earlier entry`);
|
|
|
|
|
|
seen.add(iid);
|
|
|
|
|
|
const when = String(it.when || "always");
|
|
|
|
|
|
if (!ALLOWED_WHEN.has(when)) {
|
|
|
|
|
|
throw new Error(`addon "${id}": context-menu-items[${idx}].when must be one of ${[...ALLOWED_WHEN].join("|")}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
contextMenuItems.push({
|
|
|
|
|
|
id: iid,
|
|
|
|
|
|
label: String(it.label || iid),
|
|
|
|
|
|
when,
|
|
|
|
|
|
icon: it.icon == null ? "" : String(it.icon),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit)
Aegis now covers four coins across two-step coin+network picks: BCH
(mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet).
- Sia (SC): pulled the standalone siawallet's lib into
bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing
the common adapter shape. The very first SC wallet the user adds in
Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry
over automatically; subsequent SC sub-accounts start at
"bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL
shows a "Point Aegis at a walletd node" gate in the panel.
- Vault-derive gate now honors a manifest-declared `absorbs` list, so
Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive()
guard accepts paths under either the current id or the absorbed one —
the mechanism a superseding add-on uses to inherit an older add-on's
keyspace without orphaning funds.
- DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the
SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit
(m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32.
ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool).
BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no
FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked
against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address
for "abandon×11 about, m/84'/20'/0'/0/0" is
dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39.
- Panel: SVG coin logos for SC (green disc with S) and DGB (blue
octagon with D) alongside the BCH/TRX marks. Chain-specific settings
block per coin (walletd URL for SC; derivation path for DGB). Balance
render uses BigInt-safe arithmetic so 24-decimal SC amounts don't
lose precision on the way through the panel; amount input on SC
returns a hastings string.
- Every chain adapter's snapshot fits the panel's shared shape
(address/balance/history/etc.), so future chains only need a new
chain-<x>.js file, a COINS registry entry, a matching case in
mountWallet, and an SVG logo.
Standalone siawallet addon stays as-is on disk; users can delete it once
they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
|
|
|
|
// `absorbs`: legacy add-on ids whose vault-derive namespace this add-on
|
|
|
|
|
|
// inherits. Set on a superseding add-on (e.g. aegis absorbs siawallet) so
|
|
|
|
|
|
// funds derived under the old id's paths stay reachable through the new
|
|
|
|
|
|
// one. Each entry is validated as an id itself and gates vault.derive by
|
|
|
|
|
|
// (own id OR one of these) in makeApi below.
|
|
|
|
|
|
const absorbs = Array.isArray(m.absorbs) ? m.absorbs.map(String).filter(Boolean) : [];
|
|
|
|
|
|
for (const a of absorbs) {
|
|
|
|
|
|
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(a)) {
|
|
|
|
|
|
throw new Error(`addon "${id}": absorbs entry "${a}" is not a valid add-on id`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (a === id) throw new Error(`addon "${id}": absorbs cannot list its own id`);
|
|
|
|
|
|
}
|
2026-09-14 02:30:51 +02:00
|
|
|
|
// Category: "plugin" for first-class Silent Mode components (Aegis and
|
|
|
|
|
|
// future Ariadne-as-addon) that are surfaced in Settings › Plug-ins with
|
|
|
|
|
|
// their own copy instead of the raw Extensions list. Anything else falls
|
|
|
|
|
|
// back to plain-extension rendering.
|
|
|
|
|
|
const category = m.category && ["plugin"].includes(String(m.category))
|
|
|
|
|
|
? String(m.category) : null;
|
feat(theseus/addons): context-menu-item capability + api.revealSidebar
Adds a new "context-menu-item" capability. Add-ons declare a
"context-menu-items" array in their manifest:
{
"capabilities": ["context-menu-item", ...],
"context-menu-items": [
{ "id": "translate-selection", "label": "Translate selection",
"when": "selectionText", "icon": "🌐" }
]
}
The `when` filter is one of selectionText | linkURL | editable | image
| always. Right-click on a page, and items whose `when` matches the
current context get merged into the native menu after the built-in
Search-for entry, before Back/Forward/Reload. Both context-menu
handlers (main tab area + detached link windows) share the same
merging logic.
Picking an item dispatches "context-menu" to the add-on's onMessage
handler with the full context (selectionText, linkURL, mediaType,
srcURL, pageURL, host). The add-on decides what to do — the
translate add-on stashes the selection to storage and calls
api.revealSidebar("main") which surfaces its own sidebar panel.
api.revealSidebar(panelId) is the paired hook. Ownership is enforced
by the host — an add-on can only reveal panels it registered —
before routing to main's setSidebar path.
Unknown capabilities were already silently dropped by
validateManifest, so older Theseus builds that don't understand
"context-menu-item" just ignore it, and the manifest still loads.
Add-ons that also declare "sidebar-panel" keep working; the new
capability doesn't require it.
This is the wiring that pairs with the translate/ add-on landed in
4498fbb — right-click "Translate selection" is live once this ships.
2026-09-20 18:27:10 +02:00
|
|
|
|
return { id, name, version, description, author, icon, main, capabilities, pageInject, toolbarMenu, contextMenuItems, absorbs, category };
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
|
|
|
|
|
|
// of the app queries via `getActive()` / `getInstalled()`.
|
|
|
|
|
|
class AddonHost {
|
feat(theseus/addons): context-menu-item capability + api.revealSidebar
Adds a new "context-menu-item" capability. Add-ons declare a
"context-menu-items" array in their manifest:
{
"capabilities": ["context-menu-item", ...],
"context-menu-items": [
{ "id": "translate-selection", "label": "Translate selection",
"when": "selectionText", "icon": "🌐" }
]
}
The `when` filter is one of selectionText | linkURL | editable | image
| always. Right-click on a page, and items whose `when` matches the
current context get merged into the native menu after the built-in
Search-for entry, before Back/Forward/Reload. Both context-menu
handlers (main tab area + detached link windows) share the same
merging logic.
Picking an item dispatches "context-menu" to the add-on's onMessage
handler with the full context (selectionText, linkURL, mediaType,
srcURL, pageURL, host). The add-on decides what to do — the
translate add-on stashes the selection to storage and calls
api.revealSidebar("main") which surfaces its own sidebar panel.
api.revealSidebar(panelId) is the paired hook. Ownership is enforced
by the host — an add-on can only reveal panels it registered —
before routing to main's setSidebar path.
Unknown capabilities were already silently dropped by
validateManifest, so older Theseus builds that don't understand
"context-menu-item" just ignore it, and the manifest still loads.
Add-ons that also declare "sidebar-panel" keep working; the new
capability doesn't require it.
This is the wiring that pairs with the translate/ add-on landed in
4498fbb — right-click "Translate selection" is live once this ships.
2026-09-20 18:27:10 +02:00
|
|
|
|
constructor({ addonsDir, dataDir, isDisabled, logger, setSessionProxy, vaultDerive, vaultImports, approvalModal, emitToPanel, hostRequire, hostImport, openTab, openAddonTab, openSettings, captureTab, saveCapture, checkAndStageUpdates, restartApp, revealSidebar }) {
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
this.addonsDir = addonsDir;
|
|
|
|
|
|
this.dataDir = dataDir;
|
|
|
|
|
|
this.isDisabled = isDisabled || (() => false);
|
|
|
|
|
|
this.log = logger || ((...a) => console.log("[addons]", ...a));
|
|
|
|
|
|
this._installed = []; // [{ manifest, folder, error? }]
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
this._active = new Map(); // id -> { manifest, folder, exports, sidebarPanels: [...], handlers: Map, inject }
|
|
|
|
|
|
// Capability hooks injected by main. Each is (args..., addonId) so main
|
|
|
|
|
|
// can log/gate per add-on. Missing hook = capability unavailable.
|
|
|
|
|
|
this._vaultDerive = typeof vaultDerive === "function" ? vaultDerive : null;
|
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect
Aegis Wallet 0.4.4 → 0.6.1:
- Vault lifecycle from the wallet gate. The locked / not-yet-created states
now show a master-password form (with optional BIP39 mnemonic on setup)
instead of redirecting users to Settings › Passwords. New
api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by
the existing "vault-derive" capability. api.openSettings(section) also
added; settings.html honours a #section hash on open.
- Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44
path or a WIF; the cashaddr is derived in the add-on, the signer material
goes to a separate wallet-imports.enc via api.vault.imports {list, add,
remove, signer}. Argus password-vault gains createImports / unlockImports /
saveImports with its own KDF salt so the imports key is disjoint from the
passwords key. lib/chain-bch-imported.js is a single-address Electrum
adapter; spend support is deferred to M.1b.
- Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted
in add-on storage. Fiat lines under balances, in the wallet picker, and a
portfolio total when 2+ wallets are open. Settings tab is now reachable
while the vault is locked so the toggle is always available.
- WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js).
@wizardconnect/{core,wallet} are loaded dynamically via api.import to stay
on the right side of LGPL §4d. Sign requests go through approvalModal and
are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS.
- DGB adapter load is now soft-fail: when Aegis runs from userData/addons the
bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of
taking the whole add-on down.
2026-09-09 10:33:21 +02:00
|
|
|
|
// vaultImports: main-process shim {list, add, remove, signer} that owns
|
|
|
|
|
|
// wallet-imports.enc. Same trust tier as vaultDerive — an add-on that
|
|
|
|
|
|
// holds vault-derive can also see imports (design §3.2 co-tenancy).
|
|
|
|
|
|
this._vaultImports = vaultImports && typeof vaultImports.list === "function" ? vaultImports : null;
|
|
|
|
|
|
// vaultLifecycle: main-process shim {status, setup, unlock, lock} so the
|
|
|
|
|
|
// wallet add-on can drive vault setup/unlock without redirecting users
|
|
|
|
|
|
// to Settings > Passwords. Same "vault-derive" capability gate.
|
|
|
|
|
|
this._vaultLifecycle = arguments[0].vaultLifecycle && typeof arguments[0].vaultLifecycle.unlock === "function"
|
|
|
|
|
|
? arguments[0].vaultLifecycle : null;
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
this._approvalModal = typeof approvalModal === "function" ? approvalModal : null;
|
|
|
|
|
|
this._emitToPanel = typeof emitToPanel === "function" ? emitToPanel : null;
|
|
|
|
|
|
// Add-ons live outside the app's node_modules tree, so a bare require()
|
|
|
|
|
|
// from their folder can't see Theseus's deps (ws, @noble/*, …). Main
|
|
|
|
|
|
// hands us its own require so add-ons can share the bundled tree.
|
|
|
|
|
|
this._hostRequire = typeof hostRequire === "function" ? hostRequire : null;
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
|
// ESM-only deps (@noble/*, @scure/*) can't be require()d by Electron's
|
|
|
|
|
|
// Node; hostImport resolves them from the app tree and import()s them.
|
|
|
|
|
|
this._hostImport = typeof hostImport === "function" ? hostImport : null;
|
|
|
|
|
|
this._openTab = typeof openTab === "function" ? openTab : null;
|
feat(theseus/screenshot): full-tab editor + toolbar-menu + open-tab capabilities
Reworks the screenshot addon into the flow the user asked for: the
dock icon opens a small dropdown menu (Visible viewport / Full page /
Region…) instead of the sidebar picker, and each capture opens a
full browser tab hosting an editor.
Two new addon-host capabilities land alongside:
- toolbar-menu: the addon declares an icon + item list in its manifest;
the chrome dock renders a button that, on click, opens a small menu
and dispatches the selection to the addon via addon-menu-select IPC.
- open-tab: api.openTab(path) opens a browser tab whose URL is the
addon's local file. Origin-gated per addon; the editor uses a
dedicated addon-tab-preload for its main → renderer bridge.
Editor page (editor.html/js/css):
- Crop, arrow, rectangle, circle, freehand pen, text, blur
- Colour swatches (red / yellow / acid / white / black), 3 stroke widths
- Undo/redo command stack, zoom controls
- Save PNG (goes through the download pipeline, chip picks it up)
- Copy to clipboard via ClipboardItem
2026-09-07 00:56:57 +02:00
|
|
|
|
// open-tab: opens one of the add-on's own HTML files as a full Theseus tab.
|
|
|
|
|
|
// Signature: (addonId, relPath, queryString) => Promise<void>.
|
|
|
|
|
|
this._openAddonTab = typeof openAddonTab === "function" ? openAddonTab : null;
|
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect
Aegis Wallet 0.4.4 → 0.6.1:
- Vault lifecycle from the wallet gate. The locked / not-yet-created states
now show a master-password form (with optional BIP39 mnemonic on setup)
instead of redirecting users to Settings › Passwords. New
api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by
the existing "vault-derive" capability. api.openSettings(section) also
added; settings.html honours a #section hash on open.
- Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44
path or a WIF; the cashaddr is derived in the add-on, the signer material
goes to a separate wallet-imports.enc via api.vault.imports {list, add,
remove, signer}. Argus password-vault gains createImports / unlockImports /
saveImports with its own KDF salt so the imports key is disjoint from the
passwords key. lib/chain-bch-imported.js is a single-address Electrum
adapter; spend support is deferred to M.1b.
- Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted
in add-on storage. Fiat lines under balances, in the wallet picker, and a
portfolio total when 2+ wallets are open. Settings tab is now reachable
while the vault is locked so the toggle is always available.
- WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js).
@wizardconnect/{core,wallet} are loaded dynamically via api.import to stay
on the right side of LGPL §4d. Sign requests go through approvalModal and
are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS.
- DGB adapter load is now soft-fail: when Aegis runs from userData/addons the
bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of
taking the whole add-on down.
2026-09-09 10:33:21 +02:00
|
|
|
|
// openSettings: opens Theseus's Settings tab, optionally scrolled to a
|
|
|
|
|
|
// named section (e.g. "passwords"). Uses the same IPC route the picker
|
|
|
|
|
|
// uses for "Search settings…". Signature: (section?: string) => void.
|
|
|
|
|
|
this._openSettings = typeof openSettings === "function" ? openSettings : null;
|
2026-09-14 02:30:51 +02:00
|
|
|
|
// Panel-driven self-update: an add-on may ask the host to run the
|
|
|
|
|
|
// OTA check + verify + stage flow for itself and, if a newer signed
|
|
|
|
|
|
// build lands, restart Theseus so promoteStagedUpdates picks it up.
|
|
|
|
|
|
// Owns the entire trust chain (sig, hash, manifest match) so no
|
|
|
|
|
|
// add-on ever gets to hand-write into its own installed folder.
|
|
|
|
|
|
this._checkAndStageUpdates = typeof checkAndStageUpdates === "function" ? checkAndStageUpdates : null;
|
|
|
|
|
|
this._restartApp = typeof restartApp === "function" ? restartApp : null;
|
feat(theseus/addons): context-menu-item capability + api.revealSidebar
Adds a new "context-menu-item" capability. Add-ons declare a
"context-menu-items" array in their manifest:
{
"capabilities": ["context-menu-item", ...],
"context-menu-items": [
{ "id": "translate-selection", "label": "Translate selection",
"when": "selectionText", "icon": "🌐" }
]
}
The `when` filter is one of selectionText | linkURL | editable | image
| always. Right-click on a page, and items whose `when` matches the
current context get merged into the native menu after the built-in
Search-for entry, before Back/Forward/Reload. Both context-menu
handlers (main tab area + detached link windows) share the same
merging logic.
Picking an item dispatches "context-menu" to the add-on's onMessage
handler with the full context (selectionText, linkURL, mediaType,
srcURL, pageURL, host). The add-on decides what to do — the
translate add-on stashes the selection to storage and calls
api.revealSidebar("main") which surfaces its own sidebar panel.
api.revealSidebar(panelId) is the paired hook. Ownership is enforced
by the host — an add-on can only reveal panels it registered —
before routing to main's setSidebar path.
Unknown capabilities were already silently dropped by
validateManifest, so older Theseus builds that don't understand
"context-menu-item" just ignore it, and the manifest still loads.
Add-ons that also declare "sidebar-panel" keep working; the new
capability doesn't require it.
This is the wiring that pairs with the translate/ add-on landed in
4498fbb — right-click "Translate selection" is live once this ships.
2026-09-20 18:27:10 +02:00
|
|
|
|
// revealSidebar: opens the sidebar and switches to the given panel id.
|
|
|
|
|
|
// Signature: (addonId, panelId) => void. Add-ons use this from their
|
|
|
|
|
|
// context-menu handler to surface a result in their sidebar UI.
|
|
|
|
|
|
this._revealSidebar = typeof revealSidebar === "function" ? revealSidebar : null;
|
2026-08-31 16:00:34 +02:00
|
|
|
|
// Session-proxy hook — injected by main so add-ons can swap the default
|
|
|
|
|
|
// session's proxy rules (e.g. a "route everything through my VPS" add-on).
|
|
|
|
|
|
// Signature: (rules: string | { proxyRules, proxyBypassRules }) => Promise<void>
|
|
|
|
|
|
// A `null` rule clears the proxy. Kept as a callback rather than requiring
|
|
|
|
|
|
// the loader itself import electron.
|
|
|
|
|
|
this._setSessionProxy = typeof setSessionProxy === "function" ? setSessionProxy : null;
|
2026-09-07 00:36:08 +02:00
|
|
|
|
// capture-tab hooks — main captures/saves; the loader only enforces the
|
|
|
|
|
|
// manifest gate.
|
|
|
|
|
|
this._captureTab = typeof captureTab === "function" ? captureTab : null;
|
|
|
|
|
|
this._saveCapture = typeof saveCapture === "function" ? saveCapture : null;
|
fix(theseus/boot): paint the toolbar first — stop gating startup on chrome.html's load event
Users saw a blank window with a white strip across the top for seconds
on launch. Root cause: every part of startup, including session restore,
waited for chrome.html's did-finish-load. That event also waits for the
page's subresources, and the bookmarks bar loads its favicons over
bns:// — a BNS lookup plus a network fetch each — so a slow link held the
whole boot. On top of that, seven hidden overlay renderers, every restored
tab, the BNS index build and three network fetches all started in the
same tick and stalled the main thread ~1 s while the toolbar tried to
paint.
- Continue boot at chrome.html's dom-ready (toolbar scripts have run, IPC
listeners exist) instead of did-finish-load; 8 s fallback timer.
- Window and chrome view get the toolbar's --bg for the active theme so
the pre-paint frame is never white.
- Overlay pages (site info, engine picker, downloads, suggestions,
password fill, link status, approval) load 250 ms after the toolbar or
on first use; the approval modal awaits its page so a dapp request
can't hang.
- Session restore is staggered: active tab first, then one background
tab per 150 ms slotted into its saved strip position. Session file v2
records the active index; v1 arrays still load (active = last, as the
old loop effectively did).
- AddonHost gains api.whenUiReady(); Aegis 0.6.2 defers its heavy
dependency loading (noble precompute, bitcoinjs, libauth, WizardConnect)
behind it.
- BNS snapshot warm-up still starts right after createWindow (bookmark
favicons need it); Sia refresh, update check and home-card fetch move
to the post-paint phase.
Measured on a clone of the real profile with nine restored tabs: toolbar
usable at ~0.7 s instead of ~1.5 s, main-thread stall during toolbar load
down from ~1.1 s to ~0.2 s.
2026-09-09 11:40:45 +02:00
|
|
|
|
// api.whenUiReady() plumbing — see signalUiReady().
|
|
|
|
|
|
this._uiReady = false;
|
|
|
|
|
|
this._uiReadyWaiters = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// main calls this once the browser chrome has painted. Add-ons that pull
|
|
|
|
|
|
// in heavy dependencies (Aegis: noble curve precompute, bitcoinjs, libauth,
|
|
|
|
|
|
// WizardConnect) gate that work on api.whenUiReady() so module evaluation
|
|
|
|
|
|
// doesn't land on the main thread while chrome.html is still trying to
|
|
|
|
|
|
// paint. Sticky: a later discoverAndActivate() resolves immediately.
|
|
|
|
|
|
signalUiReady() {
|
|
|
|
|
|
this._uiReady = true;
|
|
|
|
|
|
for (const resolve of this._uiReadyWaiters.splice(0)) { try { resolve(); } catch {} }
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ensureDirs() {
|
|
|
|
|
|
for (const d of [this.addonsDir, this.dataDir]) {
|
|
|
|
|
|
try { fs.mkdirSync(d, { recursive: true }); } catch (e) { this.log("mkdir failed", d, e?.message); }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect
Aegis Wallet 0.4.4 → 0.6.1:
- Vault lifecycle from the wallet gate. The locked / not-yet-created states
now show a master-password form (with optional BIP39 mnemonic on setup)
instead of redirecting users to Settings › Passwords. New
api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by
the existing "vault-derive" capability. api.openSettings(section) also
added; settings.html honours a #section hash on open.
- Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44
path or a WIF; the cashaddr is derived in the add-on, the signer material
goes to a separate wallet-imports.enc via api.vault.imports {list, add,
remove, signer}. Argus password-vault gains createImports / unlockImports /
saveImports with its own KDF salt so the imports key is disjoint from the
passwords key. lib/chain-bch-imported.js is a single-address Electrum
adapter; spend support is deferred to M.1b.
- Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted
in add-on storage. Fiat lines under balances, in the wallet picker, and a
portfolio total when 2+ wallets are open. Settings tab is now reachable
while the vault is locked so the toggle is always available.
- WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js).
@wizardconnect/{core,wallet} are loaded dynamically via api.import to stay
on the right side of LGPL §4d. Sign requests go through approvalModal and
are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS.
- DGB adapter load is now soft-fail: when Aegis runs from userData/addons the
bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of
taking the whole add-on down.
2026-09-09 10:33:21 +02:00
|
|
|
|
// api.vault.lifecycle namespace: unlock/setup/status/lock the vault. Same
|
|
|
|
|
|
// "vault-derive" cap. Purpose: let the wallet add-on drive vault setup from
|
|
|
|
|
|
// its own gate instead of redirecting users to Settings > Passwords.
|
|
|
|
|
|
_makeLifecycleApi(manifest) {
|
|
|
|
|
|
const requireCap = () => {
|
|
|
|
|
|
if (!manifest.capabilities.includes("vault-derive")) {
|
|
|
|
|
|
throw new Error(`add-on "${manifest.id}" must declare the "vault-derive" capability in addon.json`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this._vaultLifecycle) throw new Error("vault.lifecycle unavailable (host not wired)");
|
|
|
|
|
|
};
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: async () => { requireCap(); return this._vaultLifecycle.status(); },
|
|
|
|
|
|
unlock: async (pw) => { requireCap(); return this._vaultLifecycle.unlock(String(pw || ""), manifest.id); },
|
|
|
|
|
|
setup: async (pw, seedSource) => { requireCap(); return this._vaultLifecycle.setup(String(pw || ""), seedSource, manifest.id); },
|
|
|
|
|
|
lock: async () => { requireCap(); return this._vaultLifecycle.lock(manifest.id); },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// api.vault.imports namespace factory. Gated by the "vault-derive" cap
|
|
|
|
|
|
// because the two surfaces sit at the same trust tier (design §3.2). If
|
|
|
|
|
|
// main didn't wire the vaultImports shim, calls throw a clear error.
|
|
|
|
|
|
_makeImportsApi(manifest) {
|
|
|
|
|
|
const requireCap = () => {
|
|
|
|
|
|
if (!manifest.capabilities.includes("vault-derive")) {
|
|
|
|
|
|
throw new Error(`add-on "${manifest.id}" must declare the "vault-derive" capability in addon.json`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this._vaultImports) throw new Error("vault.imports unavailable (host not wired)");
|
|
|
|
|
|
};
|
|
|
|
|
|
return {
|
|
|
|
|
|
list: async () => { requireCap(); return this._vaultImports.list(); },
|
|
|
|
|
|
add: async (spec) => { requireCap(); return this._vaultImports.add(spec, manifest.id); },
|
|
|
|
|
|
remove: async (id) => { requireCap(); return this._vaultImports.remove(String(id || ""), manifest.id); },
|
|
|
|
|
|
signer: async (id) => { requireCap(); return this._vaultImports.signer(String(id || ""), manifest.id); },
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
discoverAndActivate() {
|
|
|
|
|
|
this.ensureDirs();
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
this._deactivateAll();
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
this._installed = [];
|
|
|
|
|
|
let entries = [];
|
|
|
|
|
|
try { entries = fs.readdirSync(this.addonsDir, { withFileTypes: true }); } catch { entries = []; }
|
|
|
|
|
|
for (const dirent of entries) {
|
|
|
|
|
|
if (!dirent.isDirectory()) continue;
|
|
|
|
|
|
const folder = path.join(this.addonsDir, dirent.name);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const manifest = this._readManifest(folder, dirent.name);
|
|
|
|
|
|
this._installed.push({ manifest, folder });
|
|
|
|
|
|
if (this.isDisabled(manifest.id)) {
|
|
|
|
|
|
this.log(`skipping disabled add-on ${manifest.id}`);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
this._activateOne(manifest, folder);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
this.log(`failed to load ${dirent.name}: ${e?.message || e}`);
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
|
// A manifest that parsed but whose activate() threw was already
|
|
|
|
|
|
// pushed above — replace it rather than listing the add-on twice.
|
|
|
|
|
|
const i = this._installed.findIndex((x) => x.folder === folder);
|
|
|
|
|
|
const entry = { manifest: null, folder, error: String(e?.message || e) };
|
|
|
|
|
|
if (i >= 0) this._installed[i] = entry; else this._installed.push(entry);
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.snapshot();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_readManifest(folder, folderName) {
|
|
|
|
|
|
const p = path.join(folder, "addon.json");
|
|
|
|
|
|
const raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
|
|
|
|
return validateManifest(raw, folderName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_activateOne(manifest, folder) {
|
|
|
|
|
|
const mainPath = path.join(folder, manifest.main);
|
|
|
|
|
|
// require() from a folder outside asar is fine — Electron just uses Node's
|
|
|
|
|
|
// resolver. This is where the trust decision lives: we're loading arbitrary
|
|
|
|
|
|
// JS into the main process with full API access.
|
|
|
|
|
|
let mod;
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Bust the require cache so a manual reload (future feature) picks up
|
|
|
|
|
|
// edits — cheap since add-ons are small.
|
|
|
|
|
|
delete require.cache[require.resolve(mainPath)];
|
|
|
|
|
|
mod = require(mainPath);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
throw new Error(`require() failed: ${e?.message || e}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!mod || typeof mod.activate !== "function") {
|
|
|
|
|
|
throw new Error(`main file must export an activate(api) function`);
|
|
|
|
|
|
}
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
const active = { manifest, folder, exports: mod, sidebarPanels: [], handlers: new Map(), inject: null };
|
|
|
|
|
|
if (manifest.pageInject) {
|
|
|
|
|
|
// Read the inject source once at activation. It's shipped to every
|
|
|
|
|
|
// matching tab's preload verbatim, so a syntax error surfaces in the
|
|
|
|
|
|
// tab's console, not here — but a missing file is fatal for the add-on.
|
|
|
|
|
|
const abs = path.join(folder, manifest.pageInject.preload);
|
|
|
|
|
|
let source;
|
|
|
|
|
|
try { source = fs.readFileSync(abs, "utf8"); }
|
|
|
|
|
|
catch (e) { throw new Error(`page-inject preload not readable: ${abs} (${e?.message || e})`); }
|
|
|
|
|
|
active.inject = { source, matchers: manifest.pageInject.matchers, origins: manifest.pageInject.origins };
|
|
|
|
|
|
}
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
const api = this._makeApi(active);
|
|
|
|
|
|
try { mod.activate(api); }
|
|
|
|
|
|
catch (e) { throw new Error(`activate() threw: ${e?.message || e}`); }
|
|
|
|
|
|
this._active.set(manifest.id, active);
|
|
|
|
|
|
this.log(`activated ${manifest.id} v${manifest.version}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
// Tear down every active add-on before a re-discover so long-lived state
|
|
|
|
|
|
// (sockets, timers) from a previous activation doesn't pile up.
|
|
|
|
|
|
_deactivateAll() {
|
|
|
|
|
|
for (const [id, active] of this._active) {
|
|
|
|
|
|
try { if (typeof active.exports.deactivate === "function") active.exports.deactivate(); }
|
|
|
|
|
|
catch (e) { this.log(`[${id}] deactivate() threw: ${e?.message || e}`); }
|
|
|
|
|
|
}
|
|
|
|
|
|
this._active.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
_makeApi(active) {
|
|
|
|
|
|
const { manifest, folder } = active;
|
|
|
|
|
|
const storageFile = path.join(this.dataDir, `${manifest.id}.json`);
|
|
|
|
|
|
return {
|
|
|
|
|
|
// Metadata the add-on may want to reflect on
|
|
|
|
|
|
id: manifest.id,
|
|
|
|
|
|
folder,
|
2026-09-21 01:55:25 +02:00
|
|
|
|
// Per-extension data folder (<userData>/extensions-data): where the kv
|
|
|
|
|
|
// store lives and where an add-on should keep scratch files rather
|
|
|
|
|
|
// than guessing the path from its own folder.
|
|
|
|
|
|
dataDir: this.dataDir,
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
log: (...a) => this.log(`[${manifest.id}]`, ...a),
|
|
|
|
|
|
// Persistent per-add-on storage. Small kv JSON on disk.
|
|
|
|
|
|
storage: {
|
|
|
|
|
|
get: (key, fallback = null) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const raw = JSON.parse(fs.readFileSync(storageFile, "utf8"));
|
|
|
|
|
|
return key in raw ? raw[key] : fallback;
|
|
|
|
|
|
} catch { return fallback; }
|
|
|
|
|
|
},
|
|
|
|
|
|
set: (key, value) => {
|
|
|
|
|
|
let store = {};
|
|
|
|
|
|
try { store = JSON.parse(fs.readFileSync(storageFile, "utf8")); } catch {}
|
|
|
|
|
|
store[key] = value;
|
|
|
|
|
|
try { fs.writeFileSync(storageFile, JSON.stringify(store)); } catch (e) { this.log(`[${manifest.id}] storage.set failed:`, e?.message); }
|
|
|
|
|
|
},
|
|
|
|
|
|
all: () => { try { return JSON.parse(fs.readFileSync(storageFile, "utf8")); } catch { return {}; } },
|
|
|
|
|
|
},
|
|
|
|
|
|
// Register a sidebar panel — a right-side WebContentsView that hosts
|
|
|
|
|
|
// one of the add-on's HTML pages. `page` is a path RELATIVE to the
|
|
|
|
|
|
// add-on folder. `title` shows in the sidebar tab strip. `icon` is
|
|
|
|
|
|
// a short emoji/glyph.
|
|
|
|
|
|
registerSidebarPanel: ({ id, title, icon = manifest.icon, page }) => {
|
|
|
|
|
|
if (!id || !title || !page) throw new Error(`registerSidebarPanel needs {id, title, page}`);
|
|
|
|
|
|
const abs = path.join(folder, String(page).replace(/^[\\/]/, ""));
|
|
|
|
|
|
if (!fs.existsSync(abs)) throw new Error(`sidebar panel page not found: ${abs}`);
|
|
|
|
|
|
// Namespaced id so two add-ons can't collide.
|
|
|
|
|
|
const panelId = `${manifest.id}:${id}`;
|
|
|
|
|
|
active.sidebarPanels.push({ panelId, title, icon, pageFile: abs, addonId: manifest.id });
|
|
|
|
|
|
this.log(`[${manifest.id}] registered sidebar panel: ${panelId}`);
|
|
|
|
|
|
},
|
feat(theseus/addons): context-menu-item capability + api.revealSidebar
Adds a new "context-menu-item" capability. Add-ons declare a
"context-menu-items" array in their manifest:
{
"capabilities": ["context-menu-item", ...],
"context-menu-items": [
{ "id": "translate-selection", "label": "Translate selection",
"when": "selectionText", "icon": "🌐" }
]
}
The `when` filter is one of selectionText | linkURL | editable | image
| always. Right-click on a page, and items whose `when` matches the
current context get merged into the native menu after the built-in
Search-for entry, before Back/Forward/Reload. Both context-menu
handlers (main tab area + detached link windows) share the same
merging logic.
Picking an item dispatches "context-menu" to the add-on's onMessage
handler with the full context (selectionText, linkURL, mediaType,
srcURL, pageURL, host). The add-on decides what to do — the
translate add-on stashes the selection to storage and calls
api.revealSidebar("main") which surfaces its own sidebar panel.
api.revealSidebar(panelId) is the paired hook. Ownership is enforced
by the host — an add-on can only reveal panels it registered —
before routing to main's setSidebar path.
Unknown capabilities were already silently dropped by
validateManifest, so older Theseus builds that don't understand
"context-menu-item" just ignore it, and the manifest still loads.
Add-ons that also declare "sidebar-panel" keep working; the new
capability doesn't require it.
This is the wiring that pairs with the translate/ add-on landed in
4498fbb — right-click "Translate selection" is live once this ships.
2026-09-20 18:27:10 +02:00
|
|
|
|
// Programmatically open the sidebar and switch to one of THIS add-on's
|
|
|
|
|
|
// panels. `panelId` is the un-namespaced id passed to registerSidebarPanel
|
|
|
|
|
|
// (e.g. "main"); host prefixes with the add-on id under the hood. Used
|
|
|
|
|
|
// by context-menu handlers to surface a result in the sidebar. No-op if
|
|
|
|
|
|
// the add-on doesn't own that panel or the host isn't wired.
|
|
|
|
|
|
revealSidebar: (panelId) => {
|
|
|
|
|
|
if (!this._revealSidebar) { this.log(`[${manifest.id}] revealSidebar unavailable (host not wired)`); return; }
|
|
|
|
|
|
const pid = String(panelId || "").trim();
|
|
|
|
|
|
const full = pid.includes(":") ? pid : `${manifest.id}:${pid || (active.sidebarPanels[0] && active.sidebarPanels[0].panelId.split(":")[1])}`;
|
|
|
|
|
|
const owned = active.sidebarPanels.some((p) => p.panelId === full);
|
|
|
|
|
|
if (!owned) { this.log(`[${manifest.id}] revealSidebar: no such panel ${full}`); return; }
|
|
|
|
|
|
this._revealSidebar(manifest.id, full);
|
|
|
|
|
|
},
|
2026-08-31 16:00:34 +02:00
|
|
|
|
// Swap the default session's proxy. Rules follow Chromium's proxy
|
|
|
|
|
|
// format ("socks5://1.2.3.4:1080" for a single SOCKS server,
|
|
|
|
|
|
// "http=1.2.3.4:8080;https=5.6.7.8:8080" for scheme-split HTTP, etc.).
|
|
|
|
|
|
// Pass null to clear. Add-ons that opt into this capability are
|
|
|
|
|
|
// fully replacing the browser's outgoing network path — they're
|
|
|
|
|
|
// the trust boundary while active. Same-signature as the built-in
|
|
|
|
|
|
// Tor toggle uses under the hood.
|
|
|
|
|
|
setSessionProxy: async (rules) => {
|
|
|
|
|
|
if (!this._setSessionProxy) {
|
|
|
|
|
|
this.log(`[${manifest.id}] setSessionProxy unavailable (host not wired)`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!manifest.capabilities.includes("session-proxy")) {
|
|
|
|
|
|
throw new Error(`add-on "${manifest.id}" must declare the "session-proxy" capability in addon.json`);
|
|
|
|
|
|
}
|
|
|
|
|
|
await this._setSessionProxy(rules, manifest.id);
|
|
|
|
|
|
},
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
// Resolve a module from Theseus's own dependency tree. Add-ons run with
|
|
|
|
|
|
// the app's full privileges anyway; this only saves them from shipping
|
|
|
|
|
|
// a second copy of ws / @noble / etc.
|
|
|
|
|
|
require: (name) => {
|
|
|
|
|
|
if (!this._hostRequire) throw new Error(`api.require unavailable (host not wired)`);
|
|
|
|
|
|
return this._hostRequire(name);
|
|
|
|
|
|
},
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
|
// Same, for ES-module-only packages: resolves to a Promise of the
|
|
|
|
|
|
// module namespace.
|
|
|
|
|
|
import: async (name) => {
|
|
|
|
|
|
if (!this._hostImport) throw new Error(`api.import unavailable (host not wired)`);
|
|
|
|
|
|
return this._hostImport(name);
|
|
|
|
|
|
},
|
feat(theseus/screenshot): full-tab editor + toolbar-menu + open-tab capabilities
Reworks the screenshot addon into the flow the user asked for: the
dock icon opens a small dropdown menu (Visible viewport / Full page /
Region…) instead of the sidebar picker, and each capture opens a
full browser tab hosting an editor.
Two new addon-host capabilities land alongside:
- toolbar-menu: the addon declares an icon + item list in its manifest;
the chrome dock renders a button that, on click, opens a small menu
and dispatches the selection to the addon via addon-menu-select IPC.
- open-tab: api.openTab(path) opens a browser tab whose URL is the
addon's local file. Origin-gated per addon; the editor uses a
dedicated addon-tab-preload for its main → renderer bridge.
Editor page (editor.html/js/css):
- Crop, arrow, rectangle, circle, freehand pen, text, blur
- Colour swatches (red / yellow / acid / white / black), 3 stroke widths
- Undo/redo command stack, zoom controls
- Save PNG (goes through the download pipeline, chip picks it up)
- Copy to clipboard via ClipboardItem
2026-09-07 00:56:57 +02:00
|
|
|
|
// Open a new Theseus tab. Two shapes:
|
|
|
|
|
|
// - api.openTab("https://…") — no capability needed
|
|
|
|
|
|
// - api.openTab("editor.html", { query: {...} }) — opens one of the
|
|
|
|
|
|
// add-on's OWN files as a full tab; requires the "open-tab" cap.
|
|
|
|
|
|
// Path is resolved inside the add-on folder and rejected if it
|
|
|
|
|
|
// escapes it (path traversal). Query is URL-encoded. The page
|
|
|
|
|
|
// loads under addon-tab-preload.js so window.silentmode.invoke()
|
|
|
|
|
|
// reaches the same handlers as a sidebar panel — main gates by
|
|
|
|
|
|
// sender URL so a page hosted anywhere else gets nothing back.
|
|
|
|
|
|
openTab: (pathOrUrl, opts) => {
|
|
|
|
|
|
const s = String(pathOrUrl || "");
|
|
|
|
|
|
// Bare http(s) URL with no opts — legacy behaviour, unchanged.
|
|
|
|
|
|
if (/^https?:\/\//i.test(s) && !opts) {
|
|
|
|
|
|
if (!this._openTab) throw new Error("openTab unavailable (host not wired)");
|
|
|
|
|
|
this._openTab(s, manifest.id);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!manifest.capabilities.includes("open-tab")) {
|
|
|
|
|
|
throw new Error(`add-on "${manifest.id}" must declare the "open-tab" capability in addon.json to open its own files in a tab`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this._openAddonTab) throw new Error("openAddonTab unavailable (host not wired)");
|
|
|
|
|
|
if (!s || path.isAbsolute(s) || s.includes("..")) {
|
|
|
|
|
|
throw new Error(`openTab: path must be a relative file inside the add-on folder (got "${s}")`);
|
|
|
|
|
|
}
|
|
|
|
|
|
let qs = "";
|
|
|
|
|
|
if (opts && opts.query && typeof opts.query === "object") {
|
|
|
|
|
|
const usp = new URLSearchParams();
|
|
|
|
|
|
for (const [k, v] of Object.entries(opts.query)) usp.append(String(k), String(v));
|
|
|
|
|
|
qs = usp.toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
return this._openAddonTab(manifest.id, s, qs);
|
feat(theseus/bchwallet): receive + history — vault-derived keys, cashaddr, QR, electrum
Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
2026-09-06 02:46:41 +02:00
|
|
|
|
},
|
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect
Aegis Wallet 0.4.4 → 0.6.1:
- Vault lifecycle from the wallet gate. The locked / not-yet-created states
now show a master-password form (with optional BIP39 mnemonic on setup)
instead of redirecting users to Settings › Passwords. New
api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by
the existing "vault-derive" capability. api.openSettings(section) also
added; settings.html honours a #section hash on open.
- Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44
path or a WIF; the cashaddr is derived in the add-on, the signer material
goes to a separate wallet-imports.enc via api.vault.imports {list, add,
remove, signer}. Argus password-vault gains createImports / unlockImports /
saveImports with its own KDF salt so the imports key is disjoint from the
passwords key. lib/chain-bch-imported.js is a single-address Electrum
adapter; spend support is deferred to M.1b.
- Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted
in add-on storage. Fiat lines under balances, in the wallet picker, and a
portfolio total when 2+ wallets are open. Settings tab is now reachable
while the vault is locked so the toggle is always available.
- WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js).
@wizardconnect/{core,wallet} are loaded dynamically via api.import to stay
on the right side of LGPL §4d. Sign requests go through approvalModal and
are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS.
- DGB adapter load is now soft-fail: when Aegis runs from userData/addons the
bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of
taking the whole add-on down.
2026-09-09 10:33:21 +02:00
|
|
|
|
// Open Theseus's Settings tab, optionally scrolled to a named section
|
|
|
|
|
|
// (validated against a known list in main). No capability needed —
|
|
|
|
|
|
// it's the same thing the user could do from the ⋮ menu, just a
|
|
|
|
|
|
// one-click shortcut so add-ons can point users at the right place
|
|
|
|
|
|
// (e.g. Aegis's "Set up vault" gate → Passwords).
|
|
|
|
|
|
openSettings: (section) => {
|
|
|
|
|
|
if (!this._openSettings) throw new Error("openSettings unavailable (host not wired)");
|
|
|
|
|
|
this._openSettings(typeof section === "string" ? section : "");
|
|
|
|
|
|
},
|
2026-09-14 02:30:51 +02:00
|
|
|
|
// Check the OTA channel for a newer signed build of THIS add-on and
|
|
|
|
|
|
// stage it if one is found. Returns { status, staged, current, next }
|
|
|
|
|
|
// — status matches the shared addon-updater report vocabulary
|
|
|
|
|
|
// ("up-to-date" | "staged" | "already-staged" | "fetch-failed" | …).
|
|
|
|
|
|
// The staged copy activates on the next Theseus launch, so pair with
|
|
|
|
|
|
// restartApp() when the caller wants an immediate apply. Scoped to
|
|
|
|
|
|
// the calling add-on so a plug-in can't stage updates for its
|
|
|
|
|
|
// neighbours.
|
|
|
|
|
|
checkAndStageSelfUpdate: async () => {
|
|
|
|
|
|
if (!this._checkAndStageUpdates) throw new Error("checkAndStageSelfUpdate unavailable (host not wired)");
|
|
|
|
|
|
const full = await this._checkAndStageUpdates();
|
|
|
|
|
|
const own = (full?.report || []).find((r) => r.id === manifest.id) || { status: "no-update-url" };
|
|
|
|
|
|
return {
|
|
|
|
|
|
status: own.status || "unknown",
|
|
|
|
|
|
detail: own.detail || null,
|
|
|
|
|
|
current: own.currentVer || manifest.version,
|
|
|
|
|
|
next: own.newVer || null,
|
|
|
|
|
|
staged: (full?.staged || []).find((s) => s.id === manifest.id) || null,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
// Cleanly relaunch Theseus. Used by the plug-in card's "apply
|
|
|
|
|
|
// update" chip to activate a staged build without asking the user
|
|
|
|
|
|
// to hunt for the app menu.
|
|
|
|
|
|
restartApp: () => {
|
|
|
|
|
|
if (!this._restartApp) throw new Error("restartApp unavailable (host not wired)");
|
|
|
|
|
|
this._restartApp();
|
|
|
|
|
|
},
|
fix(theseus/boot): paint the toolbar first — stop gating startup on chrome.html's load event
Users saw a blank window with a white strip across the top for seconds
on launch. Root cause: every part of startup, including session restore,
waited for chrome.html's did-finish-load. That event also waits for the
page's subresources, and the bookmarks bar loads its favicons over
bns:// — a BNS lookup plus a network fetch each — so a slow link held the
whole boot. On top of that, seven hidden overlay renderers, every restored
tab, the BNS index build and three network fetches all started in the
same tick and stalled the main thread ~1 s while the toolbar tried to
paint.
- Continue boot at chrome.html's dom-ready (toolbar scripts have run, IPC
listeners exist) instead of did-finish-load; 8 s fallback timer.
- Window and chrome view get the toolbar's --bg for the active theme so
the pre-paint frame is never white.
- Overlay pages (site info, engine picker, downloads, suggestions,
password fill, link status, approval) load 250 ms after the toolbar or
on first use; the approval modal awaits its page so a dapp request
can't hang.
- Session restore is staggered: active tab first, then one background
tab per 150 ms slotted into its saved strip position. Session file v2
records the active index; v1 arrays still load (active = last, as the
old loop effectively did).
- AddonHost gains api.whenUiReady(); Aegis 0.6.2 defers its heavy
dependency loading (noble precompute, bitcoinjs, libauth, WizardConnect)
behind it.
- BNS snapshot warm-up still starts right after createWindow (bookmark
favicons need it); Sia refresh, update check and home-card fetch move
to the post-paint phase.
Measured on a clone of the real profile with nine restored tabs: toolbar
usable at ~0.7 s instead of ~1.5 s, main-thread stall during toolbar load
down from ~1.1 s to ~0.2 s.
2026-09-09 11:40:45 +02:00
|
|
|
|
// Resolves once the browser chrome has painted (immediately if it
|
|
|
|
|
|
// already has). Put expensive dependency loading behind this so it
|
|
|
|
|
|
// never competes with the first frame at launch.
|
|
|
|
|
|
whenUiReady: () => (this._uiReady ? Promise.resolve() : new Promise((resolve) => this._uiReadyWaiters.push(resolve))),
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
// Panel ↔ activate() messaging. Panels (and, for page-inject add-ons,
|
|
|
|
|
|
// injected page bridges) call into the add-on with a message name +
|
|
|
|
|
|
// one JSON payload; the handler's return value goes back as the
|
|
|
|
|
|
// response. `ctx.from` is "panel" or "page"; pages also carry
|
|
|
|
|
|
// `ctx.origin` ("https://host") so the add-on can scope permissions.
|
|
|
|
|
|
onMessage: (msg, handler) => {
|
|
|
|
|
|
if (typeof msg !== "string" || !msg || typeof handler !== "function") throw new Error(`onMessage needs (name, fn)`);
|
|
|
|
|
|
active.handlers.set(msg, handler);
|
|
|
|
|
|
},
|
|
|
|
|
|
// Push an event to the add-on's own sidebar panel if it's currently
|
|
|
|
|
|
// loaded. Fire-and-forget; silently dropped when the panel is closed.
|
|
|
|
|
|
emit: (msg, payload) => {
|
|
|
|
|
|
if (this._emitToPanel) this._emitToPanel(manifest.id, String(msg), payload);
|
|
|
|
|
|
},
|
|
|
|
|
|
// vault-derive: a 32-byte HKDF child of the password vault's root,
|
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit)
Aegis now covers four coins across two-step coin+network picks: BCH
(mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet).
- Sia (SC): pulled the standalone siawallet's lib into
bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing
the common adapter shape. The very first SC wallet the user adds in
Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry
over automatically; subsequent SC sub-accounts start at
"bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL
shows a "Point Aegis at a walletd node" gate in the panel.
- Vault-derive gate now honors a manifest-declared `absorbs` list, so
Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive()
guard accepts paths under either the current id or the absorbed one —
the mechanism a superseding add-on uses to inherit an older add-on's
keyspace without orphaning funds.
- DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the
SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit
(m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32.
ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool).
BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no
FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked
against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address
for "abandon×11 about, m/84'/20'/0'/0/0" is
dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39.
- Panel: SVG coin logos for SC (green disc with S) and DGB (blue
octagon with D) alongside the BCH/TRX marks. Chain-specific settings
block per coin (walletd URL for SC; derivation path for DGB). Balance
render uses BigInt-safe arithmetic so 24-decimal SC amounts don't
lose precision on the way through the panel; amount input on SC
returns a hastings string.
- Every chain adapter's snapshot fits the panel's shared shape
(address/balance/history/etc.), so future chains only need a new
chain-<x>.js file, a COINS registry entry, a matching case in
mountWallet, and an SVG logo.
Standalone siawallet addon stays as-is on disk; users can delete it once
they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
|
|
|
|
// keyed by a path that MUST start with this add-on's id — or one of
|
|
|
|
|
|
// the ids it declared under `absorbs` in addon.json, so a superseding
|
|
|
|
|
|
// add-on can keep deriving the same keys as the add-on it replaced
|
|
|
|
|
|
// (funds stay reachable across the transition). Resolves only once
|
|
|
|
|
|
// the user has unlocked the vault (main polls; the await can be long).
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
vault: {
|
|
|
|
|
|
derive: async (purposePath) => {
|
|
|
|
|
|
if (!manifest.capabilities.includes("vault-derive")) {
|
|
|
|
|
|
throw new Error(`add-on "${manifest.id}" must declare the "vault-derive" capability in addon.json`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this._vaultDerive) throw new Error(`vault.derive unavailable (host not wired)`);
|
|
|
|
|
|
const p = String(purposePath || "");
|
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit)
Aegis now covers four coins across two-step coin+network picks: BCH
(mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet).
- Sia (SC): pulled the standalone siawallet's lib into
bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing
the common adapter shape. The very first SC wallet the user adds in
Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry
over automatically; subsequent SC sub-accounts start at
"bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL
shows a "Point Aegis at a walletd node" gate in the panel.
- Vault-derive gate now honors a manifest-declared `absorbs` list, so
Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive()
guard accepts paths under either the current id or the absorbed one —
the mechanism a superseding add-on uses to inherit an older add-on's
keyspace without orphaning funds.
- DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the
SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit
(m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32.
ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool).
BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no
FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked
against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address
for "abandon×11 about, m/84'/20'/0'/0/0" is
dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39.
- Panel: SVG coin logos for SC (green disc with S) and DGB (blue
octagon with D) alongside the BCH/TRX marks. Chain-specific settings
block per coin (walletd URL for SC; derivation path for DGB). Balance
render uses BigInt-safe arithmetic so 24-decimal SC amounts don't
lose precision on the way through the panel; amount input on SC
returns a hastings string.
- Every chain adapter's snapshot fits the panel's shared shape
(address/balance/history/etc.), so future chains only need a new
chain-<x>.js file, a COINS registry entry, a matching case in
mountWallet, and an SVG logo.
Standalone siawallet addon stays as-is on disk; users can delete it once
they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
|
|
|
|
if (/[^a-z0-9/._-]/i.test(p) || p.includes("..")) {
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
throw new Error(`vault.derive: purposePath must look like "${manifest.id}/<name>"`);
|
|
|
|
|
|
}
|
feat(theseus/aegis): fold Sia into the addon; add DGB (BIP84 native SegWit)
Aegis now covers four coins across two-step coin+network picks: BCH
(mainnet + chipnet), TRX (mainnet + Nile), SC (mainnet), DGB (mainnet).
- Sia (SC): pulled the standalone siawallet's lib into
bundled-addons/bchwallet/lib/sia/ and wrote lib/chain-sia.js exposing
the common adapter shape. The very first SC wallet the user adds in
Aegis reuses purpose "siawallet/mainnet/0" so pre-Aegis funds carry
over automatically; subsequent SC sub-accounts start at
"bchwallet/sc/mainnet/1". Per-wallet walletd URL setting; empty URL
shows a "Point Aegis at a walletd node" gate in the panel.
- Vault-derive gate now honors a manifest-declared `absorbs` list, so
Aegis's addon.json can list `absorbs: ["siawallet"]` and the derive()
guard accepts paths under either the current id or the absorbed one —
the mechanism a superseding add-on uses to inherit an older add-on's
keyspace without orphaning funds.
- DigiByte (DGB): lib/chain-dgb.js ports the relevant bits of the
SilentCode Digibyte design — SLIP-44 coin type 20, BIP84 native SegWit
(m/84'/20'/0'/0/x → dgb1q…) via ripemd160(sha256(pubkey)) + bech32.
ElectrumX-DGB backend reuses lib/electrum.js (public wss:50022 pool).
BIP143 P2WPKH sighash + witness-tx serialize implemented inline (no
FORKID — DGB uses standard Bitcoin sighash). Derivation cross-checked
against a known BIP39 vector in scratchpad/verify-dgb.mjs — the address
for "abandon×11 about, m/84'/20'/0'/0/0" is
dgb1q9gmf0pv8jdymcly6lz6fl7lf6mhslsd72e2jq8, matching iancoleman.io/bip39.
- Panel: SVG coin logos for SC (green disc with S) and DGB (blue
octagon with D) alongside the BCH/TRX marks. Chain-specific settings
block per coin (walletd URL for SC; derivation path for DGB). Balance
render uses BigInt-safe arithmetic so 24-decimal SC amounts don't
lose precision on the way through the panel; amount input on SC
returns a hastings string.
- Every chain adapter's snapshot fits the panel's shared shape
(address/balance/history/etc.), so future chains only need a new
chain-<x>.js file, a COINS registry entry, a matching case in
mountWallet, and an SVG logo.
Standalone siawallet addon stays as-is on disk; users can delete it once
they've confirmed Aegis shows the same balance. Nothing here disables it.
2026-09-07 01:56:25 +02:00
|
|
|
|
const allowed = [manifest.id, ...(manifest.absorbs || [])];
|
|
|
|
|
|
if (!allowed.some((prefix) => p.startsWith(prefix + "/"))) {
|
|
|
|
|
|
const list = allowed.length > 1
|
|
|
|
|
|
? `one of "${allowed.join('", "')}"`
|
|
|
|
|
|
: `"${manifest.id}"`;
|
|
|
|
|
|
throw new Error(`vault.derive: purposePath must start with ${list} + "/"`);
|
|
|
|
|
|
}
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
return this._vaultDerive(p, manifest.id);
|
|
|
|
|
|
},
|
feat(theseus/aegis): 0.6.1 — in-panel vault setup/unlock, BCH wallet imports, opt-in fiat prices, WizardConnect
Aegis Wallet 0.4.4 → 0.6.1:
- Vault lifecycle from the wallet gate. The locked / not-yet-created states
now show a master-password form (with optional BIP39 mnemonic on setup)
instead of redirecting users to Settings › Passwords. New
api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by
the existing "vault-derive" capability. api.openSettings(section) also
added; settings.html honours a #section hash on open.
- Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44
path or a WIF; the cashaddr is derived in the add-on, the signer material
goes to a separate wallet-imports.enc via api.vault.imports {list, add,
remove, signer}. Argus password-vault gains createImports / unlockImports /
saveImports with its own KDF salt so the imports key is disjoint from the
passwords key. lib/chain-bch-imported.js is a single-address Electrum
adapter; spend support is deferred to M.1b.
- Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted
in add-on storage. Fiat lines under balances, in the wallet picker, and a
portfolio total when 2+ wallets are open. Settings tab is now reachable
while the vault is locked so the toggle is always available.
- WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js).
@wizardconnect/{core,wallet} are loaded dynamically via api.import to stay
on the right side of LGPL §4d. Sign requests go through approvalModal and
are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS.
- DGB adapter load is now soft-fail: when Aegis runs from userData/addons the
bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of
taking the whole add-on down.
2026-09-09 10:33:21 +02:00
|
|
|
|
imports: this._makeImportsApi(manifest),
|
|
|
|
|
|
lifecycle: this._makeLifecycleApi(manifest),
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
},
|
|
|
|
|
|
// approval-modal: ask the user. Resolves to the chosen action id, or
|
|
|
|
|
|
// "cancel" (Escape / mask click / window closed). With `checkbox` set
|
2026-09-06 12:43:17 +02:00
|
|
|
|
// and ticked, the id comes back suffixed "+<checkbox.id>"; with
|
|
|
|
|
|
// `select` {id, label, options:[{value,label}]} and a non-empty value
|
|
|
|
|
|
// chosen, "+<select.id>=<value>".
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
approvalModal: async (opts) => {
|
|
|
|
|
|
if (!manifest.capabilities.includes("approval-modal")) {
|
|
|
|
|
|
throw new Error(`add-on "${manifest.id}" must declare the "approval-modal" capability in addon.json`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this._approvalModal) throw new Error(`approvalModal unavailable (host not wired)`);
|
|
|
|
|
|
return this._approvalModal(opts || {}, manifest.id);
|
|
|
|
|
|
},
|
2026-09-07 00:36:08 +02:00
|
|
|
|
// capture-tab: snapshot the currently-active tab.
|
|
|
|
|
|
// opts.mode "visible" | "full" | "region" (required)
|
|
|
|
|
|
// opts.format "png" | "jpeg" (default "png")
|
|
|
|
|
|
// opts.quality 1-100 (jpeg only, default 90)
|
|
|
|
|
|
// opts.overlaySource string (region only — DOM
|
|
|
|
|
|
// code the add-on wants injected while the user
|
|
|
|
|
|
// drags a selection. Must resolve to `{x,y,w,h}`
|
|
|
|
|
|
// in CSS pixels; return null/undefined to cancel.)
|
|
|
|
|
|
// Resolves to `{ dataUrl, width, height, host, format }`.
|
|
|
|
|
|
captureTab: async (opts) => {
|
|
|
|
|
|
if (!manifest.capabilities.includes("capture-tab")) {
|
|
|
|
|
|
throw new Error(`add-on "${manifest.id}" must declare the "capture-tab" capability in addon.json`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this._captureTab) throw new Error(`captureTab unavailable (host not wired)`);
|
|
|
|
|
|
return this._captureTab(opts || {}, manifest.id);
|
|
|
|
|
|
},
|
|
|
|
|
|
// capture-tab: route an in-memory image into the app's downloads pipeline
|
|
|
|
|
|
// so it lands in the user's Downloads folder AND shows up in the
|
|
|
|
|
|
// download-chip list the same way any HTTP download would.
|
|
|
|
|
|
// opts.dataUrl "data:image/png;base64,…" (required)
|
|
|
|
|
|
// opts.filename filename shown in the chip (required)
|
|
|
|
|
|
// Resolves to `{ savePath }`.
|
|
|
|
|
|
saveCapture: async (opts) => {
|
|
|
|
|
|
if (!manifest.capabilities.includes("capture-tab")) {
|
|
|
|
|
|
throw new Error(`add-on "${manifest.id}" must declare the "capture-tab" capability in addon.json`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this._saveCapture) throw new Error(`saveCapture unavailable (host not wired)`);
|
|
|
|
|
|
return this._saveCapture(opts || {}, manifest.id);
|
|
|
|
|
|
},
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
// Route a message to an add-on's registered handler. Callers (main) have
|
|
|
|
|
|
// already established WHO is asking; `ctx` carries that provenance.
|
|
|
|
|
|
async dispatch(id, msg, payload, ctx) {
|
|
|
|
|
|
const active = this._active.get(id);
|
|
|
|
|
|
if (!active) throw new Error(`add-on "${id}" is not active`);
|
|
|
|
|
|
const handler = active.handlers.get(String(msg));
|
|
|
|
|
|
if (!handler) throw new Error(`add-on "${id}" has no handler for "${msg}"`);
|
|
|
|
|
|
return handler(payload, ctx || {});
|
|
|
|
|
|
}
|
|
|
|
|
|
hasHandler(id, msg) {
|
|
|
|
|
|
const active = this._active.get(id);
|
|
|
|
|
|
return !!(active && active.handlers.has(String(msg)));
|
|
|
|
|
|
}
|
|
|
|
|
|
// Inject scripts that apply to a tab URL — [{ id, source }].
|
|
|
|
|
|
injectionsFor(url) {
|
|
|
|
|
|
const out = [];
|
|
|
|
|
|
for (const active of this._active.values()) {
|
|
|
|
|
|
if (active.inject && urlMatchesAny(url, active.inject.matchers)) {
|
|
|
|
|
|
out.push({ id: active.manifest.id, source: active.inject.source });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Does this add-on's page-inject declaration cover the URL? Used to gate
|
|
|
|
|
|
// page → add-on IPC so a non-matching page can't spoof a matching one.
|
|
|
|
|
|
pageAllowed(id, url) {
|
|
|
|
|
|
const active = this._active.get(id);
|
|
|
|
|
|
return !!(active && active.inject && urlMatchesAny(url, active.inject.matchers));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
// Read-only views for the rest of the app.
|
|
|
|
|
|
snapshot() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
installed: this._installed.map(({ manifest, folder, error }) => ({
|
|
|
|
|
|
id: manifest?.id ?? null,
|
|
|
|
|
|
name: manifest?.name ?? null,
|
|
|
|
|
|
version: manifest?.version ?? null,
|
|
|
|
|
|
description: manifest?.description ?? "",
|
|
|
|
|
|
author: manifest?.author ?? "",
|
|
|
|
|
|
icon: manifest?.icon ?? "🧩",
|
|
|
|
|
|
capabilities: manifest?.capabilities ?? [],
|
2026-09-14 02:30:51 +02:00
|
|
|
|
// "plugin" — first-class Silent Mode component (Aegis, future
|
|
|
|
|
|
// Ariadne-as-addon) surfaced in Settings › Plug-ins instead of
|
|
|
|
|
|
// the raw Extensions list. Absent → plain extension.
|
|
|
|
|
|
category: manifest?.category || null,
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
folder,
|
|
|
|
|
|
enabled: manifest?.id ? this._active.has(manifest.id) : false,
|
|
|
|
|
|
error: error || null,
|
|
|
|
|
|
})),
|
|
|
|
|
|
sidebarPanels: this.getSidebarPanels(),
|
feat(theseus/screenshot): full-tab editor + toolbar-menu + open-tab capabilities
Reworks the screenshot addon into the flow the user asked for: the
dock icon opens a small dropdown menu (Visible viewport / Full page /
Region…) instead of the sidebar picker, and each capture opens a
full browser tab hosting an editor.
Two new addon-host capabilities land alongside:
- toolbar-menu: the addon declares an icon + item list in its manifest;
the chrome dock renders a button that, on click, opens a small menu
and dispatches the selection to the addon via addon-menu-select IPC.
- open-tab: api.openTab(path) opens a browser tab whose URL is the
addon's local file. Origin-gated per addon; the editor uses a
dedicated addon-tab-preload for its main → renderer bridge.
Editor page (editor.html/js/css):
- Crop, arrow, rectangle, circle, freehand pen, text, blur
- Colour swatches (red / yellow / acid / white / black), 3 stroke widths
- Undo/redo command stack, zoom controls
- Save PNG (goes through the download pipeline, chip picks it up)
- Copy to clipboard via ClipboardItem
2026-09-07 00:56:57 +02:00
|
|
|
|
toolbarMenus: this.getToolbarMenus(),
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-09-22 00:31:18 +02:00
|
|
|
|
// `plugin` marks surfaces of a first-class Silent Mode component (manifest
|
|
|
|
|
|
// category "plugin", e.g. Aegis): the chrome pins those to their own dock
|
|
|
|
|
|
// instead of the extensions row.
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
getSidebarPanels() {
|
|
|
|
|
|
const out = [];
|
2026-09-22 00:31:18 +02:00
|
|
|
|
for (const active of this._active.values()) {
|
|
|
|
|
|
const plugin = active.manifest.category === "plugin";
|
|
|
|
|
|
for (const p of active.sidebarPanels) out.push({ ...p, plugin, addonName: active.manifest.name });
|
|
|
|
|
|
}
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
return out;
|
|
|
|
|
|
}
|
feat(theseus/screenshot): full-tab editor + toolbar-menu + open-tab capabilities
Reworks the screenshot addon into the flow the user asked for: the
dock icon opens a small dropdown menu (Visible viewport / Full page /
Region…) instead of the sidebar picker, and each capture opens a
full browser tab hosting an editor.
Two new addon-host capabilities land alongside:
- toolbar-menu: the addon declares an icon + item list in its manifest;
the chrome dock renders a button that, on click, opens a small menu
and dispatches the selection to the addon via addon-menu-select IPC.
- open-tab: api.openTab(path) opens a browser tab whose URL is the
addon's local file. Origin-gated per addon; the editor uses a
dedicated addon-tab-preload for its main → renderer bridge.
Editor page (editor.html/js/css):
- Crop, arrow, rectangle, circle, freehand pen, text, blur
- Colour swatches (red / yellow / acid / white / black), 3 stroke widths
- Undo/redo command stack, zoom controls
- Save PNG (goes through the download pipeline, chip picks it up)
- Copy to clipboard via ClipboardItem
2026-09-07 00:56:57 +02:00
|
|
|
|
// Menu declarations from every active add-on that carries a toolbar-menu
|
|
|
|
|
|
// manifest block. Chrome renders one dock button per entry, opens the
|
|
|
|
|
|
// dropdown, then dispatches "menu-select" with the picked item id.
|
|
|
|
|
|
getToolbarMenus() {
|
|
|
|
|
|
const out = [];
|
|
|
|
|
|
for (const active of this._active.values()) {
|
|
|
|
|
|
const tm = active.manifest.toolbarMenu;
|
|
|
|
|
|
if (!tm) continue;
|
|
|
|
|
|
out.push({
|
|
|
|
|
|
addonId: active.manifest.id,
|
|
|
|
|
|
title: tm.title,
|
|
|
|
|
|
icon: tm.icon,
|
2026-09-22 00:31:18 +02:00
|
|
|
|
plugin: active.manifest.category === "plugin",
|
feat(theseus/screenshot): full-tab editor + toolbar-menu + open-tab capabilities
Reworks the screenshot addon into the flow the user asked for: the
dock icon opens a small dropdown menu (Visible viewport / Full page /
Region…) instead of the sidebar picker, and each capture opens a
full browser tab hosting an editor.
Two new addon-host capabilities land alongside:
- toolbar-menu: the addon declares an icon + item list in its manifest;
the chrome dock renders a button that, on click, opens a small menu
and dispatches the selection to the addon via addon-menu-select IPC.
- open-tab: api.openTab(path) opens a browser tab whose URL is the
addon's local file. Origin-gated per addon; the editor uses a
dedicated addon-tab-preload for its main → renderer bridge.
Editor page (editor.html/js/css):
- Crop, arrow, rectangle, circle, freehand pen, text, blur
- Colour swatches (red / yellow / acid / white / black), 3 stroke widths
- Undo/redo command stack, zoom controls
- Save PNG (goes through the download pipeline, chip picks it up)
- Copy to clipboard via ClipboardItem
2026-09-07 00:56:57 +02:00
|
|
|
|
items: tm.items.map((it) => ({ id: it.id, label: it.label, icon: it.icon })),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
feat(theseus/addons): context-menu-item capability + api.revealSidebar
Adds a new "context-menu-item" capability. Add-ons declare a
"context-menu-items" array in their manifest:
{
"capabilities": ["context-menu-item", ...],
"context-menu-items": [
{ "id": "translate-selection", "label": "Translate selection",
"when": "selectionText", "icon": "🌐" }
]
}
The `when` filter is one of selectionText | linkURL | editable | image
| always. Right-click on a page, and items whose `when` matches the
current context get merged into the native menu after the built-in
Search-for entry, before Back/Forward/Reload. Both context-menu
handlers (main tab area + detached link windows) share the same
merging logic.
Picking an item dispatches "context-menu" to the add-on's onMessage
handler with the full context (selectionText, linkURL, mediaType,
srcURL, pageURL, host). The add-on decides what to do — the
translate add-on stashes the selection to storage and calls
api.revealSidebar("main") which surfaces its own sidebar panel.
api.revealSidebar(panelId) is the paired hook. Ownership is enforced
by the host — an add-on can only reveal panels it registered —
before routing to main's setSidebar path.
Unknown capabilities were already silently dropped by
validateManifest, so older Theseus builds that don't understand
"context-menu-item" just ignore it, and the manifest still loads.
Add-ons that also declare "sidebar-panel" keep working; the new
capability doesn't require it.
This is the wiring that pairs with the translate/ add-on landed in
4498fbb — right-click "Translate selection" is live once this ships.
2026-09-20 18:27:10 +02:00
|
|
|
|
// Right-click menu items declared by add-ons, filtered by the current
|
|
|
|
|
|
// context. `ctx` is what Electron's context-menu event carries:
|
|
|
|
|
|
// { selectionText, linkURL, mediaType, srcURL, isEditable, pageURL }
|
|
|
|
|
|
// Returns [{ addonId, id, label, icon, when }]. `when` filters:
|
|
|
|
|
|
// selectionText — non-empty text is selected
|
|
|
|
|
|
// linkURL — right-clicked on a link
|
|
|
|
|
|
// editable — right-clicked inside a form control / contenteditable
|
|
|
|
|
|
// image — mediaType === "image" and srcURL is present
|
|
|
|
|
|
// always — every menu
|
|
|
|
|
|
getContextMenuItems(ctx = {}) {
|
|
|
|
|
|
const has = {
|
|
|
|
|
|
selectionText: !!(ctx.selectionText && String(ctx.selectionText).trim()),
|
|
|
|
|
|
linkURL: !!ctx.linkURL,
|
|
|
|
|
|
editable: !!ctx.isEditable,
|
|
|
|
|
|
image: ctx.mediaType === "image" && !!ctx.srcURL,
|
|
|
|
|
|
};
|
|
|
|
|
|
const out = [];
|
|
|
|
|
|
for (const active of this._active.values()) {
|
|
|
|
|
|
const items = active.manifest.contextMenuItems || [];
|
|
|
|
|
|
for (const it of items) {
|
|
|
|
|
|
if (it.when !== "always" && !has[it.when]) continue;
|
|
|
|
|
|
out.push({ addonId: active.manifest.id, id: it.id, label: it.label, icon: it.icon, when: it.when });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
getInstalled() { return this._installed.slice(); }
|
|
|
|
|
|
isActive(id) { return this._active.has(id); }
|
feat(theseus/screenshot): full-tab editor + toolbar-menu + open-tab capabilities
Reworks the screenshot addon into the flow the user asked for: the
dock icon opens a small dropdown menu (Visible viewport / Full page /
Region…) instead of the sidebar picker, and each capture opens a
full browser tab hosting an editor.
Two new addon-host capabilities land alongside:
- toolbar-menu: the addon declares an icon + item list in its manifest;
the chrome dock renders a button that, on click, opens a small menu
and dispatches the selection to the addon via addon-menu-select IPC.
- open-tab: api.openTab(path) opens a browser tab whose URL is the
addon's local file. Origin-gated per addon; the editor uses a
dedicated addon-tab-preload for its main → renderer bridge.
Editor page (editor.html/js/css):
- Crop, arrow, rectangle, circle, freehand pen, text, blur
- Colour swatches (red / yellow / acid / white / black), 3 stroke widths
- Undo/redo command stack, zoom controls
- Save PNG (goes through the download pipeline, chip picks it up)
- Copy to clipboard via ClipboardItem
2026-09-07 00:56:57 +02:00
|
|
|
|
// Absolute folder of an active add-on, or null. Public so main can resolve
|
|
|
|
|
|
// add-on-relative paths (openAddonTab) without reaching into internals.
|
|
|
|
|
|
folderOf(id) { const a = this._active.get(id); return a ? a.folder : null; }
|
Theseus: add-on framework MVP + Notepad reference add-on
New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
2026-08-31 13:51:08 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(theseus/addons): vault-derive, page-inject and approval-modal capabilities
Three opt-in capabilities for add-ons, plus the plumbing they need:
- vault-derive: api.vault.derive("<id>/<path>") resolves once the password
vault is unlocked with a 32-byte HKDF child of the vault root under
"silentmode/addons/<path>". Path must start with the add-on id.
- page-inject: manifest "page-inject" {preload, origins}; a session-wide
preload asks main (sync, against the committed URL) which add-on bridges
apply and runs them in the isolated world with a scoped `theseus` object.
- approval-modal: api.approvalModal({title, body, origin, rows, actions,
checkbox}) shows a consent overlay over the tab area (approval.html);
resolves to the picked action id, "cancel", or "<id>+<checkbox>".
- api.onMessage/emit + window.silentmode.invoke/on for panel <-> activate()
messaging; page bridges use addon-page-msg, gated by tab + origin match.
- api.require so add-ons can share Theseus's dependency tree.
2026-09-06 02:33:26 +02:00
|
|
|
|
module.exports = { AddonHost, KNOWN_CAPABILITIES, validateManifest, compileOriginPattern };
|