diff --git a/addon-updater.js b/addon-updater.js index b88de1f..873341c 100644 --- a/addon-updater.js +++ b/addon-updater.js @@ -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 /addons-updates-staged/-/. +// signed version under /extensions-staged/-/. // The NEXT launch's promoteStagedUpdates() moves the staged copy into -// /addons//, reusing the same backup dance seedBundledAddons +// /extensions//, reusing the same backup dance seedBundledAddons // already uses so any local edits the user made survive. // // Trust model: diff --git a/addons-host.js b/addons-host.js index 9eeae5f..c0fbc7b 100644 --- a/addons-host.js +++ b/addons-host.js @@ -1,6 +1,6 @@ // Theseus add-on framework — loader + API surface. // -// Add-ons live in /addons// as ordinary folders on disk. Each +// Add-ons live in /extensions// 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 -// /addons-data/.json — per-add-on kv store (api.storage) +// /extensions-data/.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 (/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: { diff --git a/bundled-addons/pdf-editor/addon.json b/bundled-addons/pdf-editor/addon.json index d0899b1..e6e1e9e 100644 --- a/bundled-addons/pdf-editor/addon.json +++ b/bundled-addons/pdf-editor/addon.json @@ -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": "📄", diff --git a/bundled-addons/pdf-editor/index.js b/bundled-addons/pdf-editor/index.js index b84cb92..e7b36ca 100644 --- a/bundled-addons/pdf-editor/index.js +++ b/bundled-addons/pdf-editor/index.js @@ -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 /addons-data/, a sibling of the + // Per-add-on scratch dir under /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 (/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); } diff --git a/bundled-addons/screenshot/addon.json b/bundled-addons/screenshot/addon.json index 71da99d..a9635fc 100644 --- a/bundled-addons/screenshot/addon.json +++ b/bundled-addons/screenshot/addon.json @@ -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": "📸", diff --git a/bundled-addons/screenshot/index.js b/bundled-addons/screenshot/index.js index 04f13e0..699c9aa 100644 --- a/bundled-addons/screenshot/index.js +++ b/bundled-addons/screenshot/index.js @@ -30,12 +30,14 @@ module.exports = { page: "panel.html", }); - // Per-add-on scratch dir under /addons-data/. We don't touch + // Per-add-on scratch dir under /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 = /addons/ + // are inspecting the shipped source. api.folder = /extensions/ // screenshot/, so dirname twice lands on . 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 (/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); } diff --git a/docs/ADDON-UPDATES.md b/docs/ADDON-UPDATES.md index 9754f48..1715fcb 100644 --- a/docs/ADDON-UPDATES.md +++ b/docs/ADDON-UPDATES.md @@ -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 -`/addons-updates-staged/-/`, and promotes it on the +`/extensions-staged/-/`, 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 -`/addons-backups/--/`. +`/extensions-backups/--/`. ## 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 `/addons/` and boots against a hostile + A user who wipes `/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. diff --git a/main.js b/main.js index 624ee8a..7ec2d91 100644 --- a/main.js +++ b/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 \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 \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 /addons-updates- diff --git a/settings.html b/settings.html index 4e68a0c..5508eb4 100644 --- a/settings.html +++ b/settings.html @@ -594,7 +594,7 @@