theseus/bundled-addons/nostr/nostr-inject.js
Theseus Dev 9c29331e5b Theseus 0.3.131: Mail promoted to Sidebar quicklink, Nostr signer ships, panels free RAM when idle, Game.X in SM cards
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.
2026-10-30 00:38:08 +02:00

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);