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.
|
|
|
|
|
//
|
|
|
|
|
// Add-ons live in <userData>/addons/<id>/ as ordinary folders on disk. Each
|
|
|
|
|
// 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
|
|
|
|
|
// <userData>/addons-data/<id>.json — per-add-on kv store (api.storage)
|
|
|
|
|
|
|
|
|
|
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): 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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return { id, name, version, description, author, icon, main, capabilities, pageInject, toolbarMenu };
|
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/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
|
|
|
constructor({ addonsDir, dataDir, isDisabled, logger, setSessionProxy, vaultDerive, approvalModal, emitToPanel, hostRequire, hostImport, openTab, openAddonTab, captureTab, saveCapture }) {
|
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;
|
|
|
|
|
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;
|
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;
|
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); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
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}`);
|
|
|
|
|
},
|
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/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,
|
|
|
|
|
// keyed by a path that MUST start with this add-on's id so one add-on
|
|
|
|
|
// can never ask for another's material. Resolves only once the user
|
|
|
|
|
// has unlocked the vault (main polls; the await can be long).
|
|
|
|
|
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 || "");
|
|
|
|
|
if (!p.startsWith(manifest.id + "/") || /[^a-z0-9/._-]/i.test(p) || p.includes("..")) {
|
|
|
|
|
throw new Error(`vault.derive: purposePath must look like "${manifest.id}/<name>"`);
|
|
|
|
|
}
|
|
|
|
|
return this._vaultDerive(p, manifest.id);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// 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 ?? [],
|
|
|
|
|
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
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
getSidebarPanels() {
|
|
|
|
|
const out = [];
|
|
|
|
|
for (const active of this._active.values()) out.push(...active.sidebarPanels);
|
|
|
|
|
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,
|
|
|
|
|
items: tm.items.map((it) => ({ id: it.id, label: it.label, icon: it.icon })),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
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 };
|