feat(theseus): profile at %APPDATA%\Theseus, extensions under extensions\
The profile folder was Electron's default from the product name
("Theseus Navigator") and add-ons lived in addons\ under it. Now:
%APPDATA%\Theseus\extensions\ installed extensions
%APPDATA%\Theseus\extensions-data\ per-extension storage + scratch
%APPDATA%\Theseus\extensions-backups\ replaced copies
%APPDATA%\Theseus\extensions-staged\ staged updates
Both moves are one-time migrations on the first start that finds the old
layout: the profile folder is renamed (same volume, instant) or copied
when a rename is refused, with the old folder left in place in that case;
the four sub-folders are renamed before the extension host first reads
them. Nothing is deleted. THESEUS_USER_DATA still overrides everything.
The host now hands each extension its data folder as api.dataDir; the
Screenshot and PDF editor add-ons used to rebuild the old path from their
own folder for scratch files (so they recreated addons-data\ after the
move) and now use the field, with versions bumped so the bundles reseed.
This commit is contained in:
parent
5e4bd22609
commit
bd6aab02fe
9 changed files with 60 additions and 19 deletions
|
|
@ -4,9 +4,9 @@
|
|||
// background boot task, Theseus fetches that URL, expects a signed
|
||||
// updates.json manifest, verifies the signature against a hardcoded set
|
||||
// of operator pubkeys (addon-update-pubkeys.js), and stages any newer
|
||||
// signed version under <userData>/addons-updates-staged/<id>-<version>/.
|
||||
// signed version under <userData>/extensions-staged/<id>-<version>/.
|
||||
// The NEXT launch's promoteStagedUpdates() moves the staged copy into
|
||||
// <userData>/addons/<id>/, reusing the same backup dance seedBundledAddons
|
||||
// <userData>/extensions/<id>/, reusing the same backup dance seedBundledAddons
|
||||
// already uses so any local edits the user made survive.
|
||||
//
|
||||
// Trust model:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Theseus add-on framework — loader + API surface.
|
||||
//
|
||||
// Add-ons live in <userData>/addons/<id>/ as ordinary folders on disk. Each
|
||||
// Add-ons live in <userData>/extensions/<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
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
//
|
||||
// Persistence:
|
||||
// settings.disabledAddons — ids the user has toggled off
|
||||
// <userData>/addons-data/<id>.json — per-add-on kv store (api.storage)
|
||||
// <userData>/extensions-data/<id>.json — per-add-on kv store (api.storage)
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
|
@ -399,6 +399,10 @@ class AddonHost {
|
|||
// Metadata the add-on may want to reflect on
|
||||
id: manifest.id,
|
||||
folder,
|
||||
// Per-extension data folder (<userData>/extensions-data): where the kv
|
||||
// store lives and where an add-on should keep scratch files rather
|
||||
// than guessing the path from its own folder.
|
||||
dataDir: this.dataDir,
|
||||
log: (...a) => this.log(`[${manifest.id}]`, ...a),
|
||||
// Persistent per-add-on storage. Small kv JSON on disk.
|
||||
storage: {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "pdf-editor",
|
||||
"name": "PDF Editor",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Open a PDF in a full tab: read it, highlight and mark it up, reorder or rotate or drop pages, fill in its form fields, then save a new copy. Annotations are written into the saved file as real PDF content. The original is never touched.",
|
||||
"author": "Silent Mode",
|
||||
"icon": "📄",
|
||||
|
|
|
|||
|
|
@ -31,11 +31,13 @@ const MAX_BYTES = 200 * 1024 * 1024; // refuse anything absurd before we buffe
|
|||
|
||||
module.exports = {
|
||||
activate(api) {
|
||||
// Per-add-on scratch dir under <userData>/addons-data/, a sibling of the
|
||||
// Per-add-on scratch dir under <userData>/extensions-data/, a sibling of the
|
||||
// kv JSON. We never write inside the add-on folder — a user reading the
|
||||
// shipped source should see only what shipped.
|
||||
const dataParent = path.dirname(path.join(api.folder, ".."));
|
||||
const scratchDir = path.join(dataParent, "addons-data", SCRATCH_DIR);
|
||||
// api.dataDir is the host-provided per-extension data folder (<userData>/extensions-data);
|
||||
// the fallback rebuilds it for hosts that predate that field.
|
||||
const scratchDir = path.join(api.dataDir || path.join(dataParent, "extensions-data"), SCRATCH_DIR);
|
||||
try { fs.mkdirSync(scratchDir, { recursive: true }); }
|
||||
catch (e) { api.log("scratch mkdir failed:", e?.message); }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "screenshot",
|
||||
"name": "Screenshot",
|
||||
"version": "0.6.5",
|
||||
"version": "0.6.6",
|
||||
"description": "Capture the current tab — visible viewport, full page, or a rectangle you draw. Preview + annotate editor (crop, arrow, line, rect, ellipse, pen, resizable text, blur/mosaic redaction, undo, copy, save) live inside the sidebar. Expand the sidebar to full window for a canvas-sized editor.",
|
||||
"author": "Silent Mode",
|
||||
"icon": "📸",
|
||||
|
|
|
|||
|
|
@ -30,12 +30,14 @@ module.exports = {
|
|||
page: "panel.html",
|
||||
});
|
||||
|
||||
// Per-add-on scratch dir under <userData>/addons-data/. We don't touch
|
||||
// Per-add-on scratch dir under <userData>/extensions-data/. We don't touch
|
||||
// the add-on folder itself — editing it there would confuse users who
|
||||
// are inspecting the shipped source. api.folder = <userData>/addons/
|
||||
// are inspecting the shipped source. api.folder = <userData>/extensions/
|
||||
// screenshot/, so dirname twice lands on <userData>.
|
||||
const dataParent = path.dirname(path.join(api.folder, ".."));
|
||||
const scratchDir = path.join(dataParent, "addons-data", SCRATCH_DIR);
|
||||
// api.dataDir is the host-provided per-extension data folder (<userData>/extensions-data);
|
||||
// the fallback rebuilds it for hosts that predate that field.
|
||||
const scratchDir = path.join(api.dataDir || path.join(dataParent, "extensions-data"), SCRATCH_DIR);
|
||||
try { fs.mkdirSync(scratchDir, { recursive: true }); }
|
||||
catch (e) { api.log("scratch mkdir failed:", e?.message); }
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ An add-on can be updated at runtime, without waiting for the next Theseus
|
|||
release, if its `addon.json` declares an `updateURL`. The client polls
|
||||
that URL, verifies an Ed25519 signature against a hardcoded set of
|
||||
operator pubkeys, stages the new copy under
|
||||
`<userData>/addons-updates-staged/<id>-<version>/`, and promotes it on the
|
||||
`<userData>/extensions-staged/<id>-<version>/`, and promotes it on the
|
||||
next launch — reusing the same backup-then-swap logic as
|
||||
`seedBundledAddons`, so any local edits the user made survive as
|
||||
`<userData>/addons-backups/<id>-<oldver>-<timestamp>/`.
|
||||
`<userData>/extensions-backups/<id>-<oldver>-<timestamp>/`.
|
||||
|
||||
## Client boot flow
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ to have the right shape.
|
|||
- **Manifest downgrade** — a malicious mirror serves an older signed
|
||||
entry. The client refuses versions ≤ what's installed, so a rollback
|
||||
attack cannot land an older signed payload as if it were an update.
|
||||
A user who wipes `<userData>/addons/` and boots against a hostile
|
||||
A user who wipes `<userData>/extensions/` and boots against a hostile
|
||||
mirror could receive a stale-but-signed copy; the bundled fallback
|
||||
from the Theseus release will beat it via `seedBundledAddons` unless
|
||||
the bundle is older still.
|
||||
|
|
|
|||
41
main.js
41
main.js
|
|
@ -21,6 +21,24 @@ const RES_DIR = app.isPackaged ? process.resourcesPath : __dirname;
|
|||
// touches (or races) the real install's settings, vault and add-ons.
|
||||
if (process.env.THESEUS_USER_DATA) {
|
||||
try { app.setPath("userData", path.resolve(process.env.THESEUS_USER_DATA)); } catch (e) { console.warn("userData override failed:", e?.message); }
|
||||
} else {
|
||||
// The profile lives at <appData>\Theseus. Electron's default was the
|
||||
// product name ("Theseus Navigator"); a profile from before this change is
|
||||
// moved once, on the first start that finds it: a rename when possible
|
||||
// (same volume — instant, nothing copied), a copy when the rename is
|
||||
// refused (another process still holds the folder, or a junction to
|
||||
// another volume), in which case the old folder is left as it was.
|
||||
try { app.setPath("userData", relocateProfile(app.getPath("appData"))); }
|
||||
catch (e) { console.warn("profile relocation failed:", e?.message); }
|
||||
}
|
||||
function relocateProfile(appData) {
|
||||
const oldDir = path.join(appData, "Theseus Navigator");
|
||||
const newDir = path.join(appData, "Theseus");
|
||||
if (!fs.existsSync(newDir) && fs.existsSync(oldDir)) {
|
||||
try { fs.renameSync(oldDir, newDir); }
|
||||
catch { fs.cpSync(oldDir, newDir, { recursive: true }); }
|
||||
}
|
||||
return newDir;
|
||||
}
|
||||
// Bundled as .mjs so it loads as ES module in the packaged app (no package.json
|
||||
// sits next to it in resources/, so a bare .js would be treated as CommonJS and
|
||||
|
|
@ -1674,10 +1692,24 @@ let proxyLoginHandler = null;
|
|||
const { AddonHost } = require("./addons-host.js");
|
||||
const addonUpdater = require("./addon-updater.js");
|
||||
const { PUBKEYS_HEX: ADDON_UPDATE_PUBKEYS } = require("./addon-update-pubkeys.js");
|
||||
function addonsUserDir() { return path.join(app.getPath("userData"), "addons"); }
|
||||
function addonsDataDir() { return path.join(app.getPath("userData"), "addons-data"); }
|
||||
function addonsBackupDir() { return path.join(app.getPath("userData"), "addons-backups"); }
|
||||
function addonsStagedDir() { return path.join(app.getPath("userData"), "addons-updates-staged"); }
|
||||
// Extensions live under <userData>\extensions (installed copies), with their
|
||||
// per-extension storage, backups of replaced copies and staged updates in
|
||||
// sibling folders. These were addons / addons-data / addons-backups /
|
||||
// addons-updates-staged; migrateExtensionDirs() renames a profile's old
|
||||
// folders once, before the host first reads them.
|
||||
function addonsUserDir() { return path.join(app.getPath("userData"), "extensions"); }
|
||||
function addonsDataDir() { return path.join(app.getPath("userData"), "extensions-data"); }
|
||||
function addonsBackupDir() { return path.join(app.getPath("userData"), "extensions-backups"); }
|
||||
function addonsStagedDir() { return path.join(app.getPath("userData"), "extensions-staged"); }
|
||||
function migrateExtensionDirs() {
|
||||
const ud = app.getPath("userData");
|
||||
for (const [from, to] of [["addons", "extensions"], ["addons-data", "extensions-data"], ["addons-backups", "extensions-backups"], ["addons-updates-staged", "extensions-staged"]]) {
|
||||
const src = path.join(ud, from), dst = path.join(ud, to);
|
||||
if (!fs.existsSync(src) || fs.existsSync(dst)) continue;
|
||||
try { fs.renameSync(src, dst); console.log(`[addons] moved ${from} -> ${to}`); }
|
||||
catch (e) { console.warn(`[addons] could not move ${from} -> ${to}:`, e?.message); }
|
||||
}
|
||||
}
|
||||
function bundledAddonsDir() { return path.join(RES_DIR, "bundled-addons"); }
|
||||
// Copy bundled reference add-ons (shipped inside resources/) into the user's
|
||||
// addons directory. Users can then edit, disable, or delete them — the
|
||||
|
|
@ -5616,6 +5648,7 @@ if (!process.env.THESEUS_NO_AUTOSTART) {
|
|||
} catch (err) { console.warn("[bcnr] preload registration failed:", err?.message ?? err); }
|
||||
protocol.handle("bns", serveBns);
|
||||
installDownloadTracker();
|
||||
migrateExtensionDirs();
|
||||
initAddons();
|
||||
// Kick off signed add-on update polling 30 s after boot so it never
|
||||
// slows launch. Any staged update lands in <userData>/addons-updates-
|
||||
|
|
|
|||
|
|
@ -594,7 +594,7 @@
|
|||
<section id="addons" hidden>
|
||||
<h1>Extensions</h1>
|
||||
<p class="lede">Small modules that add capabilities to Theseus. Extensions live as folders under
|
||||
<code style="background:transparent;border:none;padding:0" id="addonsPathHint">%APPDATA%\Theseus Navigator\addons\</code>. Drop a folder in, restart, it's live.
|
||||
<code style="background:transparent;border:none;padding:0" id="addonsPathHint">%APPDATA%\Theseus\extensions\</code>. Drop a folder in, restart, it's live.
|
||||
Bundled reference extensions (like the Notepad) are copied there on first run — you can edit or remove them
|
||||
without losing anything the browser needs.</p>
|
||||
<div class="row" style="justify-content:flex-end;gap:8px">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue