Silent Mode Mail is no longer an extension. The 0.1.0 panel was a black
screen (iframe to 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-second "did it embed?" watchdog saw it as loaded and the
fallback never swapped in). The whole extension existed to put one "Open
Mail" button in a panel — that is exactly what a sidebar quicklink does,
without taking a panel slot or an Extensions row. So:
- settings.quickLinks default list gains silentmail at the top
(Silent Mode Mail → https://mail.silentmode.st/).
- migrateAegisRename grew a mail retirement block: any existing
extensions/mail/ folder on the user's box moves to
addons-backups/mail-migrated-<stamp>/ on first boot of this
Theseus, so the old extension vanishes from Settings instead of
sitting there with a broken panel.
- "mail" added to the first-party reserved-ids list so no community
add-on can claim the id after retirement.
Nostr NIP-07 signer finally ships. The bundled-addons/nostr/ folder was
sitting untracked in the main checkout, so the detached-HEAD build
worktree 0.3.130 used never saw it (same reason build/, tor/, snapshots/
have to be hand-copied into the worktree). This build committed the
folder and copied it in. Puts window.nostr on https + localhost pages;
identity is Hermes's key (one npub for DMs and public notes). Per-origin
read/sign grants; kinds 0/3/5/10002 re-prompt every time with no
remember option.
Sidebar panel apps free their render tree after idle. closeLeftPanel
used to keep the loaded page warm forever — great for fast reopen, bad
for RAM when the user docks a heavy panel (Hermes, Pithos, Aegis) once
and never comes back. A 2-minute idle timer now blanks the panel to
about:blank after close; reopen within the window still reuses the
loaded page (cancelLeftPanelIdleUnload on open), reopen after the
window reloads fresh. LEFT_PANEL_IDLE_MS = 2 * 60 * 1000. Right-side
sidebar view (webapp tabs — Telegram, X, WhatsApp, AI chat) is longer-
lived by design and untouched here.
Startpage layout:
- Game.X takes SilentMode.X's slot in the Silent Mode cards (stub
description + a built-in gamepad brand mark until game.x goes
live). SilentMode.X moves to the quick-links strip as the 5th
slot. Still TODO: push the swap into the remote home-cards.json
for existing profiles.
- Light-theme palette flip under body.light. The @media
(prefers-color-scheme: light) block only triggered on OS-level
light preference; picking the "light" background preset left --ink
white against a white page, so hero titles, card titles, slot
labels and the ".bch" extension read as pale ghosts. Added a full
body.light { --ink; --mut; --card; --line; ... } block plus
overrides for the hero + card translucent panels (dark-glass →
light-glass on light theme) and the ctx-menu / modal. Dark theme
untouched.
72 lines
3.1 KiB
JavaScript
72 lines
3.1 KiB
JavaScript
// window.nostr — the NIP-07 provider.
|
|
//
|
|
// Runs in the isolated world of every matching tab and exposes the standard
|
|
// surface on the page's main world. Nothing here can sign: every method is a
|
|
// round trip into the add-on, which checks the per-origin grant, may raise an
|
|
// approval modal, and only then asks the host (which holds the key).
|
|
//
|
|
// `theseus` is provided by the host: { id, origin, contextBridge, invoke }.
|
|
//
|
|
// Shape follows NIP-07 exactly, because clients feature-detect on it:
|
|
// await window.nostr.getPublicKey() -> hex string
|
|
// await window.nostr.signEvent(event) -> signed event
|
|
// await window.nostr.nip04.encrypt(pubkey, plain) -> ciphertext
|
|
// await window.nostr.nip04.decrypt(pubkey, cipher) -> plaintext
|
|
// await window.nostr.nip44.encrypt(pubkey, plain) -> ciphertext
|
|
// await window.nostr.nip44.decrypt(pubkey, cipher) -> plaintext
|
|
// getRelays() is deliberately absent — see the note at the bottom.
|
|
|
|
const call = (msg, payload) =>
|
|
theseus.invoke(msg, payload).catch((e) => {
|
|
// Electron wraps handler errors; unwrap so a client sees "denied", not
|
|
// "Error invoking remote method 'addon-page-msg': Error: denied".
|
|
const text = String((e && e.message) || e)
|
|
.replace(/^Error invoking remote method '[^']+': (Error: )?/, "");
|
|
throw new Error(text);
|
|
});
|
|
|
|
// Only plain data crosses the bridge. A page could hand us an object with
|
|
// getters or a prototype that misbehaves once it is read on the other side;
|
|
// round-tripping through JSON pins the value at the moment of the call.
|
|
function plain(value) {
|
|
try { return JSON.parse(JSON.stringify(value ?? null)); }
|
|
catch { throw new Error("that value cannot be sent to the signer"); }
|
|
}
|
|
|
|
const nostr = {
|
|
async getPublicKey() {
|
|
return call("getPublicKey", {});
|
|
},
|
|
|
|
async signEvent(event) {
|
|
if (!event || typeof event !== "object") throw new Error("signEvent: an event object is required");
|
|
return call("signEvent", { event: plain(event) });
|
|
},
|
|
|
|
nip04: {
|
|
async encrypt(pubkey, plaintext) {
|
|
return call("nip04Encrypt", { peer: String(pubkey || ""), plaintext: String(plaintext ?? "") });
|
|
},
|
|
async decrypt(pubkey, ciphertext) {
|
|
return call("nip04Decrypt", { peer: String(pubkey || ""), ciphertext: String(ciphertext ?? "") });
|
|
},
|
|
},
|
|
|
|
nip44: {
|
|
async encrypt(pubkey, plaintext) {
|
|
return call("nip44Encrypt", { peer: String(pubkey || ""), plaintext: String(plaintext ?? "") });
|
|
},
|
|
async decrypt(pubkey, ciphertext) {
|
|
return call("nip44Decrypt", { peer: String(pubkey || ""), ciphertext: String(ciphertext ?? "") });
|
|
},
|
|
},
|
|
|
|
// Not implemented on purpose. getRelays() is optional in NIP-07, and every
|
|
// client treats an absent method as "the user has no relay preference here"
|
|
// and falls back to its own list. Returning an empty object instead would
|
|
// read as "this user has zero relays", which some clients act on by
|
|
// publishing nowhere. Absent is the honest answer until Theseus exposes the
|
|
// Hermes relay set deliberately.
|
|
};
|
|
|
|
theseus.contextBridge.exposeInMainWorld("nostr", nostr);
|