Startpage layout reverted to the pre-0.3.131 shape. 0.3.131's full-page-
height sidebar was a mistake — slots in a short strip under a hero at
the top, cards and slots sharing one row of panels at the same height,
is what matches the design. .page/.main-col/.sidebar structure dropped,
hero back as a body sibling of .homepage, slots-list back to repeat(3,
72px) with minSlots=9. Two of the user's own tweaks baked into the
defaults: .section.chromeless on the left column so slot icons float
on the page background instead of sitting in a boxed panel, and
defaultState().iconSize = "large" (60 px tiles).
Game.X in the SM cards (replaces SilentMode.X in its slot, stub
description + a built-in gamepad brand mark until game.x goes live).
SilentMode.X moved to the quick-links strip as the 5th slot. moveItem
now strips the pinned-slot anchor when a q item moves to sm — a link
dragged onto the cards used to flip back on the next render because
its slot number was still set.
Nostr NIP-07 signer grows per-site identities (0.1.0 → 0.1.1). First
approval modal picks between the Hermes identity (one npub for DMs
and public notes, the one published as the np record on your BNS
name) and a site-specific HKDF-derived npub (unique per origin, so
Nostr clients can't cross-correlate your activity). The chooser shows
both npubs so the user sees what each button reveals. Grants without
a stored profileId (migrated from 0.3.131) default to the Hermes
identity so existing approvals keep behaving the same.
lib/hermes.js:nostrKeyFromRootForOrigin HKDFs with info
silentmode/nostr-origin/v1/<origin> over the existing 32-byte
messengerRoot. main.js _nostrAPI methods take {profileId, origin};
signer cache per profile.
Silent Mode Mail retired as an extension — now a Sidebar quicklink
(id: silentmail). Its 0.1.0 panel iframed mail.silentmode.st, which
sends X-Frame-Options: DENY; the browser rendered the block page and
STILL fired the iframe's load event, so the 5 s "did it embed?"
watchdog saw it as loaded and the panel stayed black. The whole
extension was just a landing page with an Open Mail button — exactly
what a sidebar quicklink does, without taking a panel slot or an
Extensions row. migrateAegisRename auto-retires any existing
extensions/mail/ to addons-backups/mail-migrated-<stamp>/ on first
boot of this Theseus; "mail" added to the first-party reserved-ids
list so a community add-on can't claim the id.
Claude connector 0.2.1 → 0.2.2: the bearer token no longer rotates on
every server stop. The old stop() generated a fresh token every time
Theseus quit, the panel toggled off, or a 0.3.X install replaced the
app — the Connect button wrote the new token to ~/.claude.json but
any already-running Claude Code or Claude Desktop process held the
previous token in memory and only re-reads config on launch, so /mcp
reconnect kept sending the stale bearer and the server rejected it
forever. Infinite loop: install Theseus upgrade, click Connect, MCP
still 401, close Claude, open Claude, install another upgrade a day
later, repeat. The explicit Revoke token button still rotates for
users who want fresh keys; everything else is persistent.
Sidebar panel apps blank to about:blank after 2 min of being closed to
free their render tree (iframes, media, listeners). Fast reopen within
the window still reuses the loaded page.
Light theme palette flip under body.light — the full set of
--ink / --mut / --card / --line tokens flip plus hero + card
translucent panels switch to light-glass, so hero titles, SM card
text, slot labels and the ".bch" tld read correctly on the light
background preset instead of as pale ghosts.
112 lines
6.2 KiB
JavaScript
112 lines
6.2 KiB
JavaScript
// Preload for tab webContents that host the built-in home page. All non-
|
|
// settings tabs get this preload since we don't know in advance whether a
|
|
// tab will land on home.html; the corresponding IPC handlers in main.js
|
|
// validate the sender's URL is our own home.html file:// and reject any
|
|
// origin-mismatched call, so a third-party page can inspect the API's
|
|
// SHAPE but can't invoke it against local user data.
|
|
const { contextBridge, ipcRenderer } = require("electron");
|
|
// Web tabs run this preload in iframes too (nodeIntegrationInSubFrames), so
|
|
// the password hooks below see logins inside frames. Everything else here
|
|
// stays top-frame only, as it always was.
|
|
if (window.top === window) {
|
|
// A click in the page closes the left panel (web app / add-on) unless it is
|
|
// pinned. Main ignores this unless a panel is open and the tab is active.
|
|
window.addEventListener("pointerdown", () => { ipcRenderer.send("tab-pointerdown"); }, true);
|
|
contextBridge.exposeInMainWorld("home", {
|
|
getCards: () => ipcRenderer.invoke("home-cards-get"),
|
|
setCards: (cards) => ipcRenderer.invoke("home-cards-set", cards),
|
|
resetCards: () => ipcRenderer.invoke("home-cards-reset"),
|
|
navigate: (url) => ipcRenderer.invoke("navigate", url),
|
|
// Current default search engine — used to populate the search box
|
|
// placeholder with the actual engine name (e.g. "Search the web with
|
|
// Startpage") instead of hardcoding DuckDuckGo.
|
|
getEngines: () => ipcRenderer.invoke("search-engines"),
|
|
// Main pushes an updated list here after a successful remote refresh
|
|
// (only when the user has not customised locally). home.html re-renders.
|
|
onCards: (cb) => ipcRenderer.on("home-cards", (_e, list) => cb(list)),
|
|
});
|
|
// Same preload also serves the branded error page (error.html). Handlers in
|
|
// main.js sender-check for error.html so a random page seeing the API shape
|
|
// can't drive navigation.
|
|
contextBridge.exposeInMainWorld("errorpage", {
|
|
retry: (url) => ipcRenderer.invoke("error-retry", url),
|
|
goHome: () => ipcRenderer.invoke("error-home"),
|
|
searchFor: (text) => ipcRenderer.invoke("error-search", text),
|
|
registerOnSirius: (host) => ipcRenderer.invoke("error-register", host),
|
|
openExternal: (url) => ipcRenderer.invoke("error-open-external", url),
|
|
});
|
|
}
|
|
|
|
// ---- Password manager hooks -------------------------------------------------
|
|
// Runs in this preload's isolated world in every frame of a web tab (a login
|
|
// form inside an iframe counts too); nothing is exposed to the page. Two
|
|
// reports go to main, which takes the site from the committed URL of the
|
|
// frame that sent them, never from here:
|
|
// pw-form a login field got focus -> main may offer saved logins under it
|
|
// pw-capture a form carrying a password was sent -> main may offer to save it
|
|
(() => {
|
|
if (!/^(https?|bns):$/.test(location.protocol)) return;
|
|
const visible = (el) => {
|
|
const r = el.getBoundingClientRect();
|
|
if (r.width < 4 || r.height < 4) return false;
|
|
const cs = getComputedStyle(el);
|
|
return cs.visibility !== "hidden" && cs.display !== "none";
|
|
};
|
|
const TEXTY = /^(text|email|tel|)$/i;
|
|
const passwords = (scope) => [...(scope || document).querySelectorAll("input[type=password]")].filter((el) => !el.disabled && visible(el));
|
|
// The username is the closest text-like field before the password in the
|
|
// same form (or page), which is how almost every login form is laid out.
|
|
function usernameFor(pw) {
|
|
const scope = pw.form || document;
|
|
const inputs = [...scope.querySelectorAll("input")].filter((el) => !el.disabled && visible(el));
|
|
const at = inputs.indexOf(pw);
|
|
for (let i = at - 1; i >= 0; i--) if (TEXTY.test(inputs[i].type || "text")) return inputs[i];
|
|
return null;
|
|
}
|
|
function capture() {
|
|
try {
|
|
const pws = passwords().filter((el) => el.value);
|
|
if (!pws.length) return;
|
|
// Sign-up: password + confirmation (same value). Change-password:
|
|
// current, new[, confirm] -> the new one is second.
|
|
const signup = pws.length >= 2 || /new-password/i.test(pws[0].autocomplete || "");
|
|
const pw = pws.length >= 3 ? pws[1] : pws.length === 2 && pws[0].value !== pws[1].value ? pws[1] : pws[0];
|
|
const user = usernameFor(pws[0]);
|
|
ipcRenderer.send("pw-capture", { username: user ? user.value : "", password: pw.value, signup });
|
|
} catch {}
|
|
}
|
|
document.addEventListener("submit", capture, true);
|
|
// Script-driven logins never fire submit: catch the button press and Enter.
|
|
document.addEventListener("click", (e) => {
|
|
const b = e.target instanceof Element ? e.target.closest("button, input[type=submit], input[type=button], [role=button]") : null;
|
|
if (b && passwords().some((el) => el.value)) capture();
|
|
}, true);
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" && e.target instanceof HTMLInputElement && (e.target.type === "password" || TEXTY.test(e.target.type))) {
|
|
if (passwords().some((el) => el.value)) capture();
|
|
}
|
|
}, true);
|
|
|
|
// A focused username or password field of a login form (one password
|
|
// field, empty) asks main to show saved logins under it.
|
|
function loginField(el) {
|
|
if (!(el instanceof HTMLInputElement) || el.disabled || el.readOnly) return null;
|
|
if (el.type === "password") return passwords(el.form || document).length === 1 ? el : null;
|
|
if (!TEXTY.test(el.type || "text")) return null;
|
|
const pws = passwords(el.form || document);
|
|
return pws.length === 1 && usernameFor(pws[0]) === el ? el : null;
|
|
}
|
|
function offer(el) {
|
|
if (!el || el.value) return;
|
|
const r = el.getBoundingClientRect();
|
|
ipcRenderer.send("pw-form", { x: r.left, y: r.top, h: r.height });
|
|
}
|
|
document.addEventListener("focusin", (e) => offer(loginField(e.target)), true);
|
|
// Typing means the user is not taking the offer.
|
|
document.addEventListener("input", (e) => { if (loginField(e.target)) ipcRenderer.send("pw-form-dismiss"); }, true);
|
|
document.addEventListener("keydown", (e) => { if (e.key === "Escape") ipcRenderer.send("pw-form-dismiss"); }, true);
|
|
// An autofocused field is already focused when this runs.
|
|
const early = () => offer(loginField(document.activeElement));
|
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", early, { once: true });
|
|
else early();
|
|
})();
|