2026-10-03 19:03:05 +02:00
|
|
|
|
// Pithos for Theseus. The add-on runs the same control server as the web
|
2026-10-03 19:34:55 +02:00
|
|
|
|
// console and desktop app, in Theseus's main process, and shows the same UI
|
|
|
|
|
|
// as a left-sidebar panel. The panel is the add-on's own ui/index.html, so
|
|
|
|
|
|
// no loopback address ever appears in the address bar: the UI's API calls,
|
|
|
|
|
|
// uploads and live events travel over the add-on IPC bridge (invoke / emit),
|
|
|
|
|
|
// and this file forwards them to the server with an internal session.
|
2026-10-03 19:03:05 +02:00
|
|
|
|
//
|
|
|
|
|
|
// Loaded with require() by the add-on host; core/ is ESM, hence import().
|
|
|
|
|
|
|
|
|
|
|
|
const path = require("node:path");
|
2026-10-03 22:10:01 +02:00
|
|
|
|
const fs = require("node:fs");
|
|
|
|
|
|
const crypto = require("node:crypto");
|
2026-10-03 19:03:05 +02:00
|
|
|
|
const { pathToFileURL } = require("node:url");
|
|
|
|
|
|
|
|
|
|
|
|
let pithos = null;
|
|
|
|
|
|
let starting = null;
|
2026-10-03 19:34:55 +02:00
|
|
|
|
let cookie = null;
|
|
|
|
|
|
let events = null; // AbortController for the /api/events relay
|
2026-10-03 19:03:05 +02:00
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
|
activate(api) {
|
|
|
|
|
|
// The pinned s3d release lands in the per-add-on data folder, never in the
|
|
|
|
|
|
// add-on folder itself (that one is replaced on update).
|
|
|
|
|
|
const dataDir = path.join(api.dataDir || path.join(api.folder, "..", "..", "extensions-data"), "pithos");
|
|
|
|
|
|
|
|
|
|
|
|
async function ensureServer() {
|
|
|
|
|
|
if (pithos) return pithos;
|
|
|
|
|
|
if (!starting) {
|
|
|
|
|
|
starting = (async () => {
|
|
|
|
|
|
const { createPithos } = await import(pathToFileURL(path.join(__dirname, "core", "server.js")).href);
|
2026-10-03 19:34:55 +02:00
|
|
|
|
const p = await createPithos({
|
2026-10-03 19:03:05 +02:00
|
|
|
|
host: "127.0.0.1",
|
|
|
|
|
|
port: 0,
|
|
|
|
|
|
hostName: "theseus",
|
|
|
|
|
|
binDir: path.join(dataDir, "bin"),
|
|
|
|
|
|
openExternal: (url) => api.openTab(url),
|
2026-10-04 00:05:50 +02:00
|
|
|
|
// "Back to sidebar" from the full-tab view.
|
|
|
|
|
|
showPanel: () => api.revealSidebar("main"),
|
2026-10-04 03:36:50 +02:00
|
|
|
|
// "On Silent Mode": sign-in identity and file encryption keys come
|
|
|
|
|
|
// from the vault, so another computer with the same vault reaches
|
|
|
|
|
|
// and reads the same drives. NEVER CHANGE THESE PATHS: encrypted
|
|
|
|
|
|
// files would become unreadable.
|
|
|
|
|
|
secrets: {
|
|
|
|
|
|
kind: "vault",
|
|
|
|
|
|
identitySeed: () => vaultKey("pithos/hosted-identity/v1", "Sign in to your drives on Silent Mode."),
|
|
|
|
|
|
encryptionKey: () => vaultKey("pithos/hosted-encryption/v1", "Open your encrypted files on Silent Mode."),
|
|
|
|
|
|
},
|
2026-10-03 19:03:05 +02:00
|
|
|
|
});
|
2026-10-03 19:34:55 +02:00
|
|
|
|
cookie = p.internalSession();
|
|
|
|
|
|
pithos = p;
|
|
|
|
|
|
api.log(`control server on ${p.url}`);
|
|
|
|
|
|
return p;
|
2026-10-03 19:03:05 +02:00
|
|
|
|
})().finally(() => { starting = null; });
|
|
|
|
|
|
}
|
|
|
|
|
|
return starting;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-03 19:34:55 +02:00
|
|
|
|
async function call(method, apiPath, { body, headers = {} } = {}) {
|
|
|
|
|
|
const p = await ensureServer();
|
|
|
|
|
|
const res = await fetch(new URL(apiPath, p.url), {
|
|
|
|
|
|
method,
|
|
|
|
|
|
headers: { cookie, "x-pithos": "1", ...headers },
|
|
|
|
|
|
body,
|
|
|
|
|
|
});
|
|
|
|
|
|
const text = await res.text();
|
|
|
|
|
|
let data = null;
|
|
|
|
|
|
try { data = text ? JSON.parse(text) : null; } catch { data = { error: text }; }
|
|
|
|
|
|
return { status: res.status, data };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Relay the server's event stream to the panel as api.emit events.
|
|
|
|
|
|
async function startEvents() {
|
|
|
|
|
|
if (events) return;
|
|
|
|
|
|
const p = await ensureServer();
|
|
|
|
|
|
const ctl = new AbortController();
|
|
|
|
|
|
events = ctl;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(new URL("/api/events", p.url), { headers: { cookie }, signal: ctl.signal });
|
|
|
|
|
|
const reader = res.body.getReader();
|
|
|
|
|
|
const dec = new TextDecoder();
|
|
|
|
|
|
let buf = "";
|
|
|
|
|
|
for (;;) {
|
|
|
|
|
|
const { value, done } = await reader.read();
|
|
|
|
|
|
if (done) break;
|
|
|
|
|
|
buf += dec.decode(value, { stream: true });
|
|
|
|
|
|
let i;
|
|
|
|
|
|
while ((i = buf.indexOf("\n\n")) >= 0) {
|
|
|
|
|
|
const block = buf.slice(0, i);
|
|
|
|
|
|
buf = buf.slice(i + 2);
|
|
|
|
|
|
const ev = /^event: (.+)$/m.exec(block);
|
|
|
|
|
|
const data = /^data: (.+)$/m.exec(block);
|
|
|
|
|
|
if (ev && data) api.emit("pithos-event", { event: ev[1], data: JSON.parse(data[1]) });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (!ctl.signal.aborted) api.log("event relay stopped:", e.message);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (events === ctl) events = null;
|
2026-10-03 19:03:05 +02:00
|
|
|
|
}
|
2026-10-03 19:34:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
api.onMessage("api", ({ method, path: apiPath, body }) =>
|
|
|
|
|
|
call(method || "GET", apiPath, body === undefined ? {} : { body: JSON.stringify(body), headers: { "content-type": "application/json" } }));
|
|
|
|
|
|
|
|
|
|
|
|
// Uploads arrive as one ArrayBuffer over IPC; forward it as the PUT body.
|
|
|
|
|
|
api.onMessage("upload", ({ path: apiPath, bytes, type }) =>
|
|
|
|
|
|
call("PUT", apiPath, { body: Buffer.from(bytes), headers: { "content-type": type || "application/octet-stream" } }));
|
|
|
|
|
|
|
|
|
|
|
|
// Hand the file to Theseus's own download manager through a one-time link.
|
|
|
|
|
|
api.onMessage("download", async ({ path: apiPath }) => {
|
|
|
|
|
|
const p = await ensureServer();
|
|
|
|
|
|
const { session } = require("electron");
|
|
|
|
|
|
session.defaultSession.downloadURL(p.downloadUrl(apiPath));
|
|
|
|
|
|
return { ok: true };
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
api.onMessage("events-start", () => { startEvents(); return { ok: true }; });
|
|
|
|
|
|
api.onMessage("open-external", ({ url }) => {
|
|
|
|
|
|
if (!/^https:\/\//.test(url)) throw new Error("only https links");
|
|
|
|
|
|
api.openTab(url);
|
|
|
|
|
|
return { ok: true };
|
2026-10-03 19:03:05 +02:00
|
|
|
|
});
|
2026-10-04 00:05:50 +02:00
|
|
|
|
// Viewer: a 10-minute inline link for one object (images, audio, video).
|
|
|
|
|
|
api.onMessage("view-url", async ({ path: apiPath } = {}) => {
|
|
|
|
|
|
const p = await ensureServer();
|
|
|
|
|
|
return { url: p.viewUrl(apiPath) };
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-04 02:38:21 +02:00
|
|
|
|
// Footer updater: stage the newest signed Pithos from the extension
|
|
|
|
|
|
// channel, then (second click) ask Theseus to restart and apply it.
|
|
|
|
|
|
// Theseus asks the user before restarting. Older Theseus builds lack
|
|
|
|
|
|
// these hooks; the panel then points to Settings > Extensions.
|
|
|
|
|
|
api.onMessage("update", async ({ step = "stage" } = {}) => {
|
2026-10-04 15:56:01 +02:00
|
|
|
|
// In place, no restart (Theseus 0.3.77+): Theseus swaps the new version in,
|
|
|
|
|
|
// restarts the add-on and reloads this panel.
|
|
|
|
|
|
if (step === "apply-now") {
|
|
|
|
|
|
if (typeof api.applySelfUpdate !== "function") return { ok: false, reason: "needs a restart" };
|
|
|
|
|
|
return await api.applySelfUpdate();
|
|
|
|
|
|
}
|
2026-10-04 02:38:21 +02:00
|
|
|
|
if (step === "apply") {
|
|
|
|
|
|
if (typeof api.restartApp !== "function") return { fallback: "settings" };
|
|
|
|
|
|
return await api.restartApp();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof api.checkAndStageSelfUpdate !== "function") return { fallback: "settings" };
|
|
|
|
|
|
const r = await api.checkAndStageSelfUpdate();
|
|
|
|
|
|
const staged = r?.status === "staged" || r?.status === "already-staged";
|
2026-10-04 15:56:01 +02:00
|
|
|
|
return { staged, status: r?.status || "unknown", detail: r?.detail || null, current: r?.current || null, next: r?.next || null, inPlace: typeof api.applySelfUpdate === "function" };
|
2026-10-04 02:38:21 +02:00
|
|
|
|
});
|
|
|
|
|
|
api.onMessage("open-settings", ({ section = "" } = {}) => { api.openSettings?.(String(section)); return { ok: true }; });
|
|
|
|
|
|
|
2026-10-04 03:29:32 +02:00
|
|
|
|
// ---- pithos.sia/<user>/<drive>/<folder>: the app at its own address ----
|
|
|
|
|
|
// Theseus hands this add-on the requests for paths under pithos.sia (the
|
|
|
|
|
|
// root page stays the website). They go to the control server with the
|
|
|
|
|
|
// internal session, but only for pithos.sia's own requests and the
|
|
|
|
|
|
// address bar, never another site's fetch or form, and only while the
|
|
|
|
|
|
// vault is unlocked, the same as the sidebar panel.
|
|
|
|
|
|
const siteRoutes = typeof api.registerSiteRoute === "function";
|
|
|
|
|
|
if (siteRoutes) {
|
|
|
|
|
|
const json = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" } });
|
|
|
|
|
|
const FORWARD = ["content-type", "x-pithos", "range", "accept", "if-range"];
|
|
|
|
|
|
const DROP = new Set(["set-cookie", "connection", "keep-alive", "transfer-encoding"]);
|
|
|
|
|
|
api.registerSiteRoute({ host: "pithos.sia", handle: async (request) => {
|
|
|
|
|
|
const u = new URL(request.url);
|
|
|
|
|
|
const fetchSite = request.headers.get("sec-fetch-site");
|
|
|
|
|
|
const origin = request.headers.get("origin");
|
|
|
|
|
|
const own = fetchSite ? fetchSite === "same-origin" || fetchSite === "none"
|
|
|
|
|
|
: !origin || /^(bns|https):\/\/pithos\.sia$/i.test(origin);
|
|
|
|
|
|
const st = await vaultStatus();
|
|
|
|
|
|
const locked = st.setup && st.prompt && !st.unlocked;
|
|
|
|
|
|
if (u.pathname === "/api/host/unlock") {
|
|
|
|
|
|
if (request.method !== "POST" || request.headers.get("x-pithos") !== "1" || !own) return json(403, { error: "forbidden" });
|
|
|
|
|
|
if (!locked) return json(200, { ok: true });
|
|
|
|
|
|
const r = await vault.requestUnlock({ reason: "Open Pithos, your S3 storage on Sia." });
|
|
|
|
|
|
return json(200, { ok: !!r.ok });
|
|
|
|
|
|
}
|
|
|
|
|
|
if (locked && u.pathname === "/api/session") return json(200, { authenticated: false, locked: true, passwordLogin: false, host: "theseus" });
|
|
|
|
|
|
const p = await ensureServer();
|
|
|
|
|
|
const headers = { "x-pithos-site": "1" };
|
|
|
|
|
|
for (const k of FORWARD) { const v = request.headers.get(k); if (v) headers[k] = v; }
|
|
|
|
|
|
if (own && !locked) headers.cookie = cookie;
|
|
|
|
|
|
const init = { method: request.method, headers, redirect: "manual" };
|
|
|
|
|
|
if (request.method !== "GET" && request.method !== "HEAD") init.body = Buffer.from(await request.arrayBuffer());
|
|
|
|
|
|
const res = await fetch(new URL(u.pathname + u.search, p.url), init);
|
|
|
|
|
|
if (res.status === 404 && res.headers.get("x-pithos-not-mine")) return null;
|
|
|
|
|
|
const out = new Headers();
|
|
|
|
|
|
for (const [k, v] of res.headers) if (!DROP.has(k.toLowerCase())) out.set(k, v);
|
|
|
|
|
|
return new Response(request.method === "HEAD" ? null : res.body, { status: res.status, headers: out });
|
|
|
|
|
|
} });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-04 04:06:04 +02:00
|
|
|
|
// An HTML file from a drive, opened as a page in a new Theseus tab. The
|
|
|
|
|
|
// control server sandboxes it (no access to Pithos or the session).
|
|
|
|
|
|
api.onMessage("open-page", async ({ path: pagePath } = {}) => {
|
|
|
|
|
|
if (!/^\/page\/[A-Za-z0-9_-]{20,}\/[^\s]*$/.test(String(pagePath || ""))) throw new Error("not a page link");
|
|
|
|
|
|
const p = await ensureServer();
|
|
|
|
|
|
api.openTab(new URL(pagePath, p.url).href);
|
|
|
|
|
|
return { ok: true };
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-04 03:29:32 +02:00
|
|
|
|
// Full page: Pithos at pithos.sia/<user>/<drive>/… in an ordinary tab.
|
|
|
|
|
|
// An older Theseus without site routes gets the loopback address.
|
|
|
|
|
|
api.onMessage("open-in-tab", async ({ path: appPath } = {}) => {
|
2026-10-03 19:34:55 +02:00
|
|
|
|
const p = await ensureServer();
|
2026-10-04 03:29:32 +02:00
|
|
|
|
const clean = /^\/[^\s]*$/.test(String(appPath || "")) && appPath !== "/" ? appPath : "/drives";
|
|
|
|
|
|
api.openTab(siteRoutes ? `https://pithos.sia${clean}` : p.authUrl);
|
2026-10-03 19:34:55 +02:00
|
|
|
|
return { ok: true };
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-03 20:42:23 +02:00
|
|
|
|
// ---- Theseus vault: PIN gate and the vault-derived recovery phrase ------
|
|
|
|
|
|
// Older Theseus builds have the vault but no requestUnlock (no PIN prompt);
|
|
|
|
|
|
// Pithos then runs ungated, as before.
|
|
|
|
|
|
const vault = api.vault || null;
|
|
|
|
|
|
const canPrompt = !!(vault && typeof vault.requestUnlock === "function");
|
|
|
|
|
|
|
|
|
|
|
|
async function vaultStatus() {
|
|
|
|
|
|
if (!vault || !vault.lifecycle) return { setup: false, unlocked: false, prompt: false };
|
|
|
|
|
|
const st = await vault.lifecycle.status();
|
|
|
|
|
|
return { setup: !!st.setup, unlocked: !!st.unlocked, prompt: canPrompt };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
api.onMessage("vault-status", () => vaultStatus());
|
|
|
|
|
|
|
2026-10-04 03:36:50 +02:00
|
|
|
|
async function vaultKey(purpose, reason) {
|
|
|
|
|
|
const st = await vaultStatus();
|
|
|
|
|
|
if (!st.setup) throw new Error("Set up the password vault in Settings › Passwords first.");
|
|
|
|
|
|
if (!st.unlocked) {
|
|
|
|
|
|
if (!canPrompt) throw new Error("Unlock the password vault in Settings › Passwords first.");
|
|
|
|
|
|
const r = await vault.requestUnlock({ reason });
|
|
|
|
|
|
if (!r.ok) throw new Error("The vault stayed locked.");
|
|
|
|
|
|
}
|
|
|
|
|
|
const bytes = await vault.derive(purpose);
|
|
|
|
|
|
const key = Buffer.from(bytes.subarray(0, 32));
|
|
|
|
|
|
bytes.fill(0);
|
|
|
|
|
|
return key;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-03 20:42:23 +02:00
|
|
|
|
// Watch for the vault locking (Settings › Lock now, or a lock from another
|
|
|
|
|
|
// extension) and tell the panel at once. This runs in the main process:
|
|
|
|
|
|
// a hidden panel's own timers are throttled to about once a minute.
|
|
|
|
|
|
if (canPrompt) {
|
|
|
|
|
|
let lastUnlocked = null;
|
|
|
|
|
|
setInterval(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const st = await vaultStatus();
|
|
|
|
|
|
if (st.unlocked !== lastUnlocked) {
|
|
|
|
|
|
lastUnlocked = st.unlocked;
|
|
|
|
|
|
api.emit("pithos-vault", st);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
}, 3000).unref?.();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Pithos shows access-key secrets and can delete buckets, so in Theseus it
|
|
|
|
|
|
// opens only with the vault unlocked: Theseus asks for the PIN (or the
|
|
|
|
|
|
// master password after three wrong tries) in its own prompt.
|
|
|
|
|
|
api.onMessage("gate", async () => {
|
|
|
|
|
|
const st = await vaultStatus();
|
|
|
|
|
|
if (!st.setup || !st.prompt || st.unlocked) return { ok: true, ...st };
|
|
|
|
|
|
const r = await vault.requestUnlock({ reason: "Open Pithos, your S3 storage on Sia." });
|
|
|
|
|
|
return { ...r, ...(await vaultStatus()) };
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Connect with a recovery phrase derived from the vault: nothing to write
|
|
|
|
|
|
// down, and restoring the vault restores access. The phrase is built and
|
|
|
|
|
|
// handed to s3d here; it never reaches the panel page.
|
|
|
|
|
|
// CHANGING THE PURPOSE PATH OR THE DERIVATION CUTS USERS OFF FROM THEIR DATA.
|
|
|
|
|
|
api.onMessage("connect-with-vault", async ({ indexerUrl } = {}) => {
|
|
|
|
|
|
const st = await vaultStatus();
|
|
|
|
|
|
if (!st.setup) throw new Error("Set up the password vault in Settings › Passwords first.");
|
|
|
|
|
|
if (!st.unlocked) {
|
|
|
|
|
|
if (!canPrompt) throw new Error("Unlock the password vault in Settings › Passwords first.");
|
|
|
|
|
|
const r = await vault.requestUnlock({ reason: "Create the recovery phrase for your Sia storage." });
|
|
|
|
|
|
if (!r.ok) throw new Error("The vault stayed locked.");
|
|
|
|
|
|
}
|
|
|
|
|
|
const bytes = await vault.derive("pithos/sia-recovery/v1");
|
|
|
|
|
|
const bip39 = api.require("bip39");
|
|
|
|
|
|
const phrase = bip39.entropyToMnemonic(Buffer.from(bytes.subarray(0, 16)).toString("hex"));
|
|
|
|
|
|
bytes.fill(0);
|
|
|
|
|
|
return call("POST", "/api/login", {
|
|
|
|
|
|
body: JSON.stringify({ indexerUrl, phrase }),
|
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-03 20:48:55 +02:00
|
|
|
|
// Left edge, beside the quick links. Theseus builds without left panels
|
|
|
|
|
|
// ignore `side` and show it in the right sidebar.
|
2026-10-03 22:10:01 +02:00
|
|
|
|
// ---- Saved credentials: vault-sealed files and the save dialog --------
|
|
|
|
|
|
// A credentials file protected "with my Theseus vault" is AES-256-GCM
|
|
|
|
|
|
// under a key derived from the vault, so it needs no extra password and
|
|
|
|
|
|
// opens only where that vault is unlocked. FIXED PATH: changing it makes
|
|
|
|
|
|
// every saved file unreadable.
|
|
|
|
|
|
async function credentialsKey() {
|
|
|
|
|
|
const st = await vaultStatus();
|
|
|
|
|
|
if (!st.setup) throw new Error("Set up the password vault in Settings › Passwords first.");
|
|
|
|
|
|
if (!st.unlocked) {
|
|
|
|
|
|
if (!canPrompt) throw new Error("Unlock the password vault in Settings › Passwords first.");
|
|
|
|
|
|
const r = await vault.requestUnlock({ reason: "Protect or open a saved Pithos credentials file." });
|
|
|
|
|
|
if (!r.ok) throw new Error("The vault stayed locked.");
|
|
|
|
|
|
}
|
|
|
|
|
|
return Buffer.from(await vault.derive("pithos/credentials/v1"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
api.onMessage("vault-seal", async ({ plaintext } = {}) => {
|
|
|
|
|
|
const key = await credentialsKey();
|
|
|
|
|
|
try {
|
|
|
|
|
|
const iv = crypto.randomBytes(12);
|
|
|
|
|
|
const c = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
|
|
|
|
const ct = Buffer.concat([c.update(String(plaintext || ""), "utf8"), c.final(), c.getAuthTag()]);
|
|
|
|
|
|
return { iv: iv.toString("base64"), ct: ct.toString("base64") };
|
|
|
|
|
|
} finally { key.fill(0); }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
api.onMessage("vault-open", async ({ iv, ct } = {}) => {
|
|
|
|
|
|
const key = await credentialsKey();
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = Buffer.from(String(ct || ""), "base64");
|
|
|
|
|
|
const d = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(String(iv || ""), "base64"));
|
|
|
|
|
|
d.setAuthTag(data.subarray(data.length - 16));
|
|
|
|
|
|
return { plaintext: Buffer.concat([d.update(data.subarray(0, data.length - 16)), d.final()]).toString("utf8") };
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
throw new Error("This file was saved with a different Theseus vault.");
|
|
|
|
|
|
} finally { key.fill(0); }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// The sidebar page has no download path of its own; ask where to save.
|
|
|
|
|
|
api.onMessage("save-file", async ({ name, text } = {}) => {
|
|
|
|
|
|
const { dialog, BrowserWindow, app } = require("electron");
|
|
|
|
|
|
const safe = String(name || "pithos-credentials.pithoskey").replace(/[\\/:*?"<>|]+/g, "_").slice(0, 120);
|
|
|
|
|
|
const r = await dialog.showSaveDialog(BrowserWindow.getFocusedWindow() || undefined, {
|
|
|
|
|
|
defaultPath: path.join(app.getPath("downloads"), safe),
|
|
|
|
|
|
filters: [{ name: "Pithos credentials", extensions: ["pithoskey"] }],
|
|
|
|
|
|
});
|
|
|
|
|
|
if (r.canceled || !r.filePath) return { saved: false };
|
|
|
|
|
|
fs.writeFileSync(r.filePath, String(text || ""), { mode: 0o600 });
|
|
|
|
|
|
return { saved: true };
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-10-04 00:05:50 +02:00
|
|
|
|
// Start the control server with Theseus, so an s3d the user left running
|
|
|
|
|
|
// comes back after a restart or an update without opening Pithos first.
|
|
|
|
|
|
ensureServer().catch((e) => api.log("control server failed to start:", e.message));
|
|
|
|
|
|
|
2026-10-03 20:48:55 +02:00
|
|
|
|
api.registerSidebarPanel({ id: "main", title: "Pithos", page: "ui/index.html", side: "left" });
|
2026-10-03 19:03:05 +02:00
|
|
|
|
|
|
|
|
|
|
// The host has no quit hook for add-ons; without this an s3d we started
|
|
|
|
|
|
// would outlive Theseus.
|
|
|
|
|
|
process.on("exit", () => { try { pithos && pithos.daemon.child && pithos.daemon.child.kill(); } catch {} });
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async deactivate() {
|
2026-10-03 19:34:55 +02:00
|
|
|
|
if (events) { events.abort(); events = null; }
|
|
|
|
|
|
if (pithos) { const p = pithos; pithos = null; cookie = null; await p.close(); }
|
2026-10-03 19:03:05 +02:00
|
|
|
|
},
|
|
|
|
|
|
};
|