Ship Theseus 0.3.13 7d88e4c4 (Ariadne toggle + 'p' record + Screenshot addon)

Setup    7d88e4c46b02448e40d6075d10f2c6688c6d60c5a9ec41b9cbcb7684f131d6e1
Portable 5a4bcc6abb21c23729d79dd600142df4f171cc3d6bf71716dae1c802ac10e48f

Bundled since 0.3.12:

1514793 - Settings > Registries gets an on/off toggle for Ariadne's
Thread (system-wide BCDN resolver for non-Theseus browsers). Query is
silent Get-ScheduledTask; toggle spawns elevated PowerShell (UAC once
per action). Three states: running / stopped / not-installed.

b16f0a1 - New BCDN 'p' record type in Argus record-picker + Theseus
serving. Reverse-proxies an upstream URL under a BCDN name, keeping
the BCDN name in the address bar; uses upstream's own DNS + public
CA + Host header (unlike 'ip' which pins IP + on-chain TLS fingerprint).
Placed after 'ip' in the apex chain, suppressed under subdomain
inheritance so a 'p' name doesn't silently proxy every subdomain.

cfec253 - Argus registrar gains buildTldRegistrationTx + TLD_BEACON +
normalizeTld exports for minting per-TLD certificates per the TLD-
registry design.

bbfc05c - Bundled Screenshot add-on: capture-tab capability + sidebar
launcher for visible / full page / region modes; saves to Downloads.
Follow-up task_b9608dc6 will rework this into a full-tab editor.

Deployed. Verified LIVE 0.3.13.
This commit is contained in:
Local Dev 2026-09-07 00:36:08 +02:00
parent 10f01644c1
commit 57d71a0996
3 changed files with 160 additions and 3 deletions

View file

@ -31,7 +31,13 @@ const KNOWN_CAPABILITIES = new Set([
// URL matches one of the origin patterns.
// approval-modal: api.approvalModal({...}) — user-facing consent dialog over
// the active tab, resolved by main.
"vault-derive", "page-inject", "approval-modal",
// 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 "*://*/*".
"vault-derive", "page-inject", "approval-modal", "capture-tab",
]);
// Chrome-style match pattern → predicate. "<scheme>://<host>/<path>" where
@ -99,7 +105,7 @@ function validateManifest(raw, folderName) {
// Loader singleton. `discoverAndActivate(opts)` returns a snapshot the rest
// of the app queries via `getActive()` / `getInstalled()`.
class AddonHost {
constructor({ addonsDir, dataDir, isDisabled, logger, setSessionProxy, vaultDerive, approvalModal, emitToPanel, hostRequire, hostImport, openTab }) {
constructor({ addonsDir, dataDir, isDisabled, logger, setSessionProxy, vaultDerive, approvalModal, emitToPanel, hostRequire, hostImport, openTab, captureTab, saveCapture }) {
this.addonsDir = addonsDir;
this.dataDir = dataDir;
this.isDisabled = isDisabled || (() => false);
@ -125,6 +131,10 @@ class AddonHost {
// 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;
// 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;
}
ensureDirs() {
@ -330,6 +340,35 @@ class AddonHost {
if (!this._approvalModal) throw new Error(`approvalModal unavailable (host not wired)`);
return this._approvalModal(opts || {}, manifest.id);
},
// 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);
},
};
}

118
main.js
View file

@ -1384,6 +1384,124 @@ function initAddons() {
hostRequire: (name) => require(name),
hostImport: (name) => import(require("node:url").pathToFileURL(require.resolve(name)).href),
openTab: (url) => { if (win) createTab(url); },
// capture-tab: three modes.
// visible — one WebContents.capturePage() of the current viewport.
// full — temporarily grow the tab's WebContentsView to the page's
// scrollHeight, capture, restore. Cheap and works for most
// pages; fixed-position headers/footers will repeat because
// they anchor to the viewport, which is a known trade-off
// (documented in the panel). Alternative would be a scroll-
// and-stitch pass; kept for a later revision.
// region — run the caller-supplied overlay source in the tab, wait
// for a rect (or null = cancel), then capturePage(rect).
captureTab: async (opts, addonId) => {
const t = activeTab();
if (!t) throw new Error("no active tab");
const wc = t.view.webContents;
const host = t?.prov?.host || (() => { try { return new URL(wc.getURL()).host; } catch { return ""; } })();
const mode = String(opts?.mode || "visible");
const format = opts?.format === "jpeg" ? "jpeg" : "png";
const quality = Math.max(1, Math.min(100, Number(opts?.quality) || 90));
const encode = (img) => format === "jpeg"
? `data:image/jpeg;base64,${img.toJPEG(quality).toString("base64")}`
: img.toDataURL();
if (mode === "visible") {
const img = await wc.capturePage();
const s = img.getSize();
console.log(`[addons] [${addonId}] captureTab visible ${s.width}x${s.height}`);
return { dataUrl: encode(img), width: s.width, height: s.height, host, format };
}
if (mode === "full") {
const dims = await wc.executeJavaScript(
`({w: Math.max(document.documentElement.scrollWidth, document.body ? document.body.scrollWidth : 0),`
+ ` h: Math.max(document.documentElement.scrollHeight, document.body ? document.body.scrollHeight : 0),`
+ ` dpr: window.devicePixelRatio || 1})`, true);
const fullW = Math.max(1, Math.min(16384, Math.floor(dims.w)));
const fullH = Math.max(1, Math.min(32768, Math.floor(dims.h)));
const prevBounds = t.view.getBounds();
try {
t.view.setBounds({ x: prevBounds.x, y: prevBounds.y, width: fullW, height: fullH });
// Let layout+paint catch up before capture. One requestAnimationFrame
// isn't enough for lazy-loaded content; a short delay gets most pages.
await new Promise((r) => setTimeout(r, 300));
const img = await wc.capturePage();
const s = img.getSize();
console.log(`[addons] [${addonId}] captureTab full ${s.width}x${s.height} (page ${fullW}x${fullH})`);
return { dataUrl: encode(img), width: s.width, height: s.height, host, format };
} finally {
try { layout(); } catch {}
}
}
if (mode === "region") {
const src = String(opts?.overlaySource || "");
if (!src) throw new Error("region capture needs opts.overlaySource");
// Overlay script runs in the target tab's world. It's expected to
// resolve (as the executeJavaScript result) with {x,y,w,h} in CSS
// pixels, or null when the user hits Escape / right-clicks.
const rect = await wc.executeJavaScript(src, true);
if (!rect || typeof rect !== "object") {
console.log(`[addons] [${addonId}] captureTab region cancelled`);
return { dataUrl: "", width: 0, height: 0, host, format, cancelled: true };
}
const r = {
x: Math.max(0, Math.floor(rect.x)),
y: Math.max(0, Math.floor(rect.y)),
width: Math.max(1, Math.floor(rect.w)),
height: Math.max(1, Math.floor(rect.h)),
};
const img = await wc.capturePage(r);
const s = img.getSize();
console.log(`[addons] [${addonId}] captureTab region ${s.width}x${s.height} @ ${r.x},${r.y}`);
return { dataUrl: encode(img), width: s.width, height: s.height, host, format };
}
throw new Error(`unknown capture mode: ${mode}`);
},
// saveCapture writes the bytes to Downloads and synthesizes a completed
// download record so the chip shows the file with a Show-in-folder link,
// just like an HTTP save. session.downloadURL(dataUrl) would go through
// will-download, but data URLs come across with a synthetic filename that
// Electron won't let us override in-flight without gymnastics — writing
// directly is deterministic and produces the same user-facing artifact.
saveCapture: async (opts, addonId) => {
const dataUrl = String(opts?.dataUrl || "");
const m = /^data:([^;,]+);base64,(.+)$/.exec(dataUrl);
if (!m) throw new Error("saveCapture: dataUrl must be base64-encoded");
const mime = m[1];
const bytes = Buffer.from(m[2], "base64");
const raw = String(opts?.filename || "screenshot.png");
// Strip path separators — add-on-provided filename must not escape the
// downloads folder.
const safe = raw.replace(/[\\/:*?"<>|]+/g, "_").slice(0, 200) || "screenshot.png";
const dlDir = app.getPath("downloads");
let target = path.join(dlDir, safe);
// Uniquify: append " (n)" before the extension if the name is taken.
if (fs.existsSync(target)) {
const ext = path.extname(safe);
const stem = safe.slice(0, safe.length - ext.length);
for (let i = 2; i < 10000; i++) {
const cand = path.join(dlDir, `${stem} (${i})${ext}`);
if (!fs.existsSync(cand)) { target = cand; break; }
}
}
try { fs.writeFileSync(target, bytes); }
catch (e) { throw new Error(`saveCapture: write failed: ${e?.message || e}`); }
const id = nextDlId++;
const rec = {
id,
filename: path.basename(target),
url: `internal://addons/${addonId}/${path.basename(target)}`,
mime,
total: bytes.length,
received: bytes.length,
state: "completed",
savePath: target,
startedAt: Date.now(),
};
downloads.unshift(rec);
emitDownloads();
console.log(`[addons] [${addonId}] saveCapture wrote ${bytes.length} bytes → ${target}`);
return { savePath: target };
},
});
addonHost.discoverAndActivate();
const snap = addonHost.snapshot();

View file

@ -1,6 +1,6 @@
{
"name": "theseus-navigator",
"version": "0.3.12",
"version": "0.3.13",
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
"author": "Silent Mode",
"main": "main.js",