diff --git a/lib/hermes.js b/lib/hermes.js new file mode 100644 index 0000000..d3aef83 --- /dev/null +++ b/lib/hermes.js @@ -0,0 +1,162 @@ +// Hermes: name-addressed NIP-17 messaging over Nostr. +// +// This module lives inside Theseus because it depends on `nostr-tools` (a +// non-trivial dep the shared library layer, Argus, shouldn't carry). Loaded +// from main.js via dynamic import — same pattern as password-vault.js. +// +// Layers, sender → wire (NIP-17 + NIP-59): +// 1. rumor = unsigned kind:14 chat event +// 2. seal = kind:13 signed by sender (rumor encrypted to recipient) +// 3. wrap = kind:1059 signed by ephemeral (seal encrypted to recipient) +// +// Only the wrap goes on the wire; the relay sees an ephemeral pubkey, an +// opaque ciphertext, and a #p tag — never the sender or the plaintext. + +import { webcrypto as wc } from "node:crypto"; +import { + finalizeEvent, generateSecretKey, getPublicKey, getEventHash, + verifyEvent, nip19, nip44, +} from "nostr-tools"; + +const enc = new TextEncoder(); + +// ---- key derivation -------------------------------------------------------- +// Same shape password-vault uses: BIP-39 → 64-byte seed → 32-byte purpose root +// via HKDF-SHA256 with `info="silentmode/"`. Different purpose string, +// so passwords/0 and messenger/0 are cryptographically disjoint — one leak +// never yields the other. + +/** BIP-39 mnemonic → 64-byte seed (PBKDF2-SHA512, 2048 iters, standard). */ +export async function bip39ToSeed(mnemonic, passphrase = "") { + const km = await wc.subtle.importKey("raw", + enc.encode(String(mnemonic).normalize("NFKD").trim()), + "PBKDF2", false, ["deriveBits"]); + const salt = enc.encode(("mnemonic" + String(passphrase || "")).normalize("NFKD")); + return new Uint8Array(await wc.subtle.deriveBits( + { name: "PBKDF2", salt, iterations: 2048, hash: "SHA-512" }, km, 512)); +} + +/** Seed → 32-byte purpose root via HKDF. Purpose string is public — the + * security comes from the seed, not the info parameter. */ +export async function seedToPurposeRoot(seedBytes, purpose) { + const key = await wc.subtle.importKey("raw", seedBytes, "HKDF", false, ["deriveBits"]); + const info = enc.encode(`silentmode/${purpose}`); + return new Uint8Array(await wc.subtle.deriveBits( + { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info }, key, 256)); +} + +/** BIP-39 mnemonic → { sk, pkHex, npub } — the Nostr identity for messaging. + * Path is "messenger/0" — reserved for Hermes in + * TheseusNavigator/ROADMAP-identity-wallet.md. */ +export async function nostrKeyFromMnemonic(mnemonic, passphrase = "") { + const seed = await bip39ToSeed(mnemonic, passphrase); + const sk = await seedToPurposeRoot(seed, "messenger/0"); + const pkHex = getPublicKey(sk); + return { sk, pkHex, npub: nip19.npubEncode(pkHex) }; +} + +// ---- record parsers -------------------------------------------------------- +// The BNS resolver is transparent: whatever the name owner published in `r` +// lands as entry.records.. Here we validate and normalise the two Hermes +// record types. +// +// np — Nostr pubkey, hex (64) or npub1… +// nr — comma/space-separated list of ws(s):// relay URLs + +const HEX_PUBKEY_RE = /^[0-9a-f]{64}$/i; + +export function parseNpRecord(raw) { + if (typeof raw !== "string" || !raw.trim()) throw new Error("np: empty or missing"); + const v = raw.trim(); + if (HEX_PUBKEY_RE.test(v)) return v.toLowerCase(); + if (v.startsWith("npub1")) { + const d = nip19.decode(v); + if (d.type !== "npub" || typeof d.data !== "string" || !HEX_PUBKEY_RE.test(d.data)) { + throw new Error("np: npub decoded to unexpected shape"); + } + return d.data.toLowerCase(); + } + throw new Error(`np: not hex(64) and not npub1…: ${v.slice(0, 24)}…`); +} + +export function parseNrRecord(raw, { defaults = [] } = {}) { + if (typeof raw !== "string" || !raw.trim()) return defaults.slice(); + const seen = new Set(); const out = []; + for (const tok of raw.split(/[,\s]+/)) { + const url = tok.trim(); + if (!/^wss?:\/\//i.test(url)) continue; + if (seen.has(url)) continue; + seen.add(url); out.push(url); + } + return out.length ? out : defaults.slice(); +} + +/** Extract Hermes recipient info from a resolver Entry. */ +export function parseHermesRecords(entry, { defaultRelays = [] } = {}) { + if (!entry || !entry.records) throw new Error("no on-chain entry for name"); + return { + npPk: parseNpRecord(entry.records.np), + relays: parseNrRecord(entry.records.nr, { defaults: defaultRelays }), + }; +} + +// ---- NIP-17 wrap / unwrap -------------------------------------------------- +const KIND_CHAT = 14; +const KIND_SEAL = 13; +const KIND_GIFT_WRAP = 1059; + +// NIP-59: randomise timestamps up to 2 days in the past so relays can't +// correlate wrap-arrival with real send-time. +const TWO_DAYS = 2 * 24 * 60 * 60; +const jitteredNow = () => + Math.floor(Date.now() / 1000) - Math.floor(Math.random() * TWO_DAYS); + +const convKey = (sk, pkHex) => nip44.v2.utils.getConversationKey(sk, pkHex); +const encrypt = (sk, pkHex, plain) => nip44.v2.encrypt(plain, convKey(sk, pkHex)); +const decrypt = (sk, pkHex, cipher) => nip44.v2.decrypt(cipher, convKey(sk, pkHex)); + +/** Wrap a plaintext chat message from senderSk to recipientPkHex. */ +export function wrapChat({ senderSk, recipientPkHex, text }) { + const senderPkHex = getPublicKey(senderSk); + + const rumor = { + kind: KIND_CHAT, + pubkey: senderPkHex, + created_at: Math.floor(Date.now() / 1000), + tags: [["p", recipientPkHex]], + content: text, + }; + rumor.id = getEventHash(rumor); + + const seal = finalizeEvent({ + kind: KIND_SEAL, + created_at: jitteredNow(), + tags: [], + content: encrypt(senderSk, recipientPkHex, JSON.stringify(rumor)), + }, senderSk); + + const ephemeralSk = generateSecretKey(); + return finalizeEvent({ + kind: KIND_GIFT_WRAP, + created_at: jitteredNow(), + tags: [["p", recipientPkHex]], + content: encrypt(ephemeralSk, recipientPkHex, JSON.stringify(seal)), + }, ephemeralSk); +} + +/** Unwrap a kind:1059 gift wrap for receiverSk. + * Returns { senderPkHex, text, createdAt } or throws on tamper. */ +export function unwrapChat({ receiverSk, wrap }) { + if (wrap.kind !== KIND_GIFT_WRAP) throw new Error(`not a gift wrap (kind ${wrap.kind})`); + if (!verifyEvent(wrap)) throw new Error("gift wrap signature invalid"); + + const seal = JSON.parse(decrypt(receiverSk, wrap.pubkey, wrap.content)); + if (seal.kind !== KIND_SEAL) throw new Error(`inner is not a seal (kind ${seal.kind})`); + if (!verifyEvent(seal)) throw new Error("seal signature invalid"); + + const rumor = JSON.parse(decrypt(receiverSk, seal.pubkey, seal.content)); + if (rumor.kind !== KIND_CHAT) throw new Error(`inner is not chat (kind ${rumor.kind})`); + if (rumor.pubkey !== seal.pubkey) throw new Error("rumor.pubkey does not match seal.pubkey"); + + return { senderPkHex: rumor.pubkey, text: rumor.content, createdAt: rumor.created_at }; +} diff --git a/lib/package.json b/lib/package.json new file mode 100644 index 0000000..a5de1fe --- /dev/null +++ b/lib/package.json @@ -0,0 +1,5 @@ +{ + "//": "This subfolder holds ES modules loaded from main.js via dynamic import.", + "//2": "Theseus's root package.json is commonjs; overriding here lets .js files here parse as ESM without renaming to .mjs.", + "type": "module" +} diff --git a/main.js b/main.js index ab66f6f..feaf6a0 100644 --- a/main.js +++ b/main.js @@ -31,6 +31,15 @@ async function loadVaultLib() { if (!vaultLib) vaultLib = await import(`file://${VAULT_MOD.replace(/\\/g, "/")}`); return vaultLib; } +// Hermes messaging module — same .mjs-in-resources / .js-in-dev pattern. +const HERMES_MOD = app.isPackaged + ? path.join(RES_DIR, "lib", "hermes.mjs") + : path.join(__dirname, "lib", "hermes.js"); +let hermesLib; +async function loadHermesLib() { + if (!hermesLib) hermesLib = await import(`file://${HERMES_MOD.replace(/\\/g, "/")}`); + return hermesLib; +} // Built-in engines. Users can also add their own (settings.customEngines, // each { id, name, url } where the url contains "%s" for the query). // Catalog of built-in engines (users pick which to enable + can add their own). @@ -1804,6 +1813,262 @@ ipcMain.handle("switch-to-bcnr", () => { return loadBns(t, activeId, host, rest || "/", tld); }); +// ---- Hermes messages panel ------------------------------------------------- +// A separate BrowserWindow (opens on Ctrl+Shift+M anywhere in Theseus). Uses +// the wallet mnemonic to derive the Nostr identity in-memory only — the seed +// and secret key are never written to disk. The panel process holds one +// WebSocket per relay in HERMES_DEFAULT_RELAYS for the receive subscription, +// and opens a per-send WebSocket for publishes. +const HERMES_DEFAULT_RELAYS = ["wss://nos.lol"]; +const HERMES_INBOX_LIMIT = 200; + +let hermesWin = null; +// State when initialised: { skHex, pkHex, npub, inbox: [], relays: Map } +// skHex is held instead of the raw Uint8Array so it can be Buffer-restored per operation +// (nostr-tools expects Uint8Array; converting on demand keeps the surface easier to reason about). +let hermesState = null; + +const hOk = (o = {}) => ({ ok: true, ...o }); +const hErr = (e) => ({ ok: false, err: String((e && e.message) || e) }); + +function hermesEmit(channel, payload) { + if (hermesWin && !hermesWin.isDestroyed()) hermesWin.webContents.send(channel, payload); +} +function hermesRecord(msg) { + hermesState.inbox.push(msg); + if (hermesState.inbox.length > HERMES_INBOX_LIMIT) { + hermesState.inbox.splice(0, hermesState.inbox.length - HERMES_INBOX_LIMIT); + } + hermesEmit("hermes-message", msg); +} +// Pushed on every relay connect/disconnect so the pill in the panel reflects +// reality without polling. Cheap; sent to the renderer whenever a socket +// transitions ready/not-ready. +function hermesEmitStatus() { + if (!hermesState) return; + let connected = 0; + for (const s of hermesState.relays.values()) if (s.ready) connected++; + hermesEmit("hermes-status-update", { + relaysConnected: connected, + relaysTotal: hermesState.relays.size, + }); +} + +// Reverse-resolve a pkHex → .bch name by scanning the (cached) BNS index. +// Populates senderName in the inbox so incoming DMs render as +// "alice.bch · 12ab…9f" instead of a naked hex string. Cache is per-hermesState +// (dropped on hermes-close) so a re-init starts clean. +async function hermesReverseResolve(pkHex) { + if (!hermesState) return null; + if (!hermesState._pkToName) hermesState._pkToName = new Map(); + if (hermesState._pkToName.has(pkHex)) return hermesState._pkToName.get(pkHex); + try { + const R = await getResolver(); + const H = await loadHermesLib(); + const idx = await R.buildIndex({ WebSocket }); + for (const [name, entry] of idx) { + const raw = entry && entry.records && entry.records.np; + if (typeof raw !== "string" || !raw.trim()) continue; + try { + const hex = H.parseNpRecord(raw); + if (hex === pkHex) { + hermesState._pkToName.set(pkHex, name); + return name; + } + } catch { /* malformed np — skip */ } + } + } catch { /* chain unreachable — leave unresolved this round */ } + hermesState._pkToName.set(pkHex, null); // negative-cache so we don't re-scan every message + return null; +} + +// One receive subscription per relay. If the socket dies we resurrect it on a +// backoff — a locked / logged-out state tears them all down cleanly. +async function hermesConnectRelay(url) { + const H = await loadHermesLib(); + const state = { ws: null, ready: false, subId: "hermes-inbox" }; + const open = () => { + if (!hermesState) return; // torn down while reconnecting + const ws = new WebSocket(url); + state.ws = ws; + ws.on("open", () => { + state.ready = true; + hermesEmitStatus(); + ws.send(JSON.stringify(["REQ", state.subId, { kinds: [1059], "#p": [hermesState.pkHex] }])); + }); + ws.on("message", async (buf) => { + let msg; try { msg = JSON.parse(buf.toString()); } catch { return; } + if (msg[0] !== "EVENT" || msg[1] !== state.subId) return; + try { + const sk = Buffer.from(hermesState.skHex, "hex"); + const opened = H.unwrapChat({ receiverSk: sk, wrap: msg[2] }); + // Dedupe: a wrap arriving from multiple relays produces the same rumor id + // (kind:14 hash), but since we don't expose the rumor id here we dedupe + // on (senderPkHex, text, createdAt) which is sufficient for MVP. + const key = opened.senderPkHex + "\0" + opened.createdAt + "\0" + opened.text; + if (hermesState._seen && hermesState._seen.has(key)) return; + hermesState._seen && hermesState._seen.add(key); + // Reverse-resolve pk → name off the wrap decrypt path (fire-and-forget + // wouldn't work — we need the name in the record we push). Await here; + // the cache short-circuits after the first miss per pk. + const senderName = await hermesReverseResolve(opened.senderPkHex); + hermesRecord({ + senderPkHex: opened.senderPkHex, + senderName, + text: opened.text, + createdAt: opened.createdAt, + }); + } catch { /* unwrap failure = not for us, or malformed — drop silently */ } + }); + ws.on("close", () => { + state.ready = false; + hermesEmitStatus(); + // Attempt reconnect if we're still supposed to be running. + if (hermesState && hermesState.relays.get(url) === state) { + setTimeout(open, 3000); + } + }); + ws.on("error", () => { /* close handler will retry */ }); + }; + open(); + return state; +} + +function hermesTeardown() { + if (!hermesState) return; + for (const state of hermesState.relays.values()) { + try { state.ws?.close(1000); } catch {} + } + hermesState = null; +} + +ipcMain.handle("hermes-status", () => { + if (!hermesState) return hOk({ ready: false }); + let connected = 0; + for (const s of hermesState.relays.values()) if (s.ready) connected++; + return hOk({ + ready: true, + npub: hermesState.npub, + pkHex: hermesState.pkHex, + relaysConnected: connected, + relaysTotal: hermesState.relays.size, + }); +}); + +ipcMain.handle("hermes-init", async (_e, { mnemonic } = {}) => { + if (!mnemonic || typeof mnemonic !== "string" || mnemonic.trim().split(/\s+/).length < 12) { + return hErr("enter a 12- or 24-word mnemonic"); + } + try { + hermesTeardown(); + const H = await loadHermesLib(); + const { sk, pkHex, npub } = await H.nostrKeyFromMnemonic(mnemonic.trim()); + hermesState = { + skHex: Buffer.from(sk).toString("hex"), + pkHex, npub, + inbox: [], + relays: new Map(), + _seen: new Set(), + }; + for (const url of HERMES_DEFAULT_RELAYS) { + hermesState.relays.set(url, await hermesConnectRelay(url)); + } + // Give sockets a beat to open before reporting connected count. + await new Promise((r) => setTimeout(r, 400)); + let connected = 0; + for (const s of hermesState.relays.values()) if (s.ready) connected++; + return hOk({ npub, pkHex, relaysConnected: connected, relaysTotal: hermesState.relays.size }); + } catch (e) { hermesTeardown(); return hErr(e); } +}); + +ipcMain.handle("hermes-close", () => { hermesTeardown(); return hOk(); }); + +ipcMain.handle("hermes-inbox", () => { + if (!hermesState) return hErr("not initialised"); + return hOk({ messages: hermesState.inbox.slice() }); +}); + +// Send: `to` is either a 64-char hex pubkey or a .bch name. Names are resolved +// via the portable resolver (getResolver above), the `np` record parsed, then +// wrapped + published to each relay listed in `nr` (falling back to defaults). +ipcMain.handle("hermes-send", async (_e, { to, text } = {}) => { + if (!hermesState) return hErr("not initialised"); + if (!to || !text) return hErr("to and text required"); + try { + const H = await loadHermesLib(); + let recipientPkHex; let relays = HERMES_DEFAULT_RELAYS.slice(); + const trimmed = String(to).trim(); + if (/^[0-9a-f]{64}$/i.test(trimmed)) { + recipientPkHex = trimmed.toLowerCase(); + } else if (trimmed.startsWith("npub1")) { + recipientPkHex = H.parseNpRecord(trimmed); + } else { + const R = await getResolver(); + const name = R.normalizeName(trimmed); + const entry = await R.resolveName(name, { WebSocket }); + if (!entry) return hErr(`no on-chain registration for ${name}`); + const parsed = H.parseHermesRecords(entry, { defaultRelays: HERMES_DEFAULT_RELAYS }); + recipientPkHex = parsed.npPk; + relays = parsed.relays; + } + + const sk = Buffer.from(hermesState.skHex, "hex"); + const wrap = H.wrapChat({ senderSk: sk, recipientPkHex, text }); + + const publishOne = (url) => new Promise((resolve) => { + const ws = new WebSocket(url); + let settled = false; + const done = (r) => { if (settled) return; settled = true; try { ws.close(1000); } catch {} resolve(r); }; + const t = setTimeout(() => done({ url, ok: false, detail: "timeout" }), 8000); + ws.on("open", () => ws.send(JSON.stringify(["EVENT", wrap]))); + ws.on("message", (buf) => { + let msg; try { msg = JSON.parse(buf.toString()); } catch { return; } + if (msg[0] === "OK" && msg[1] === wrap.id) { + clearTimeout(t); + done({ url, ok: !!msg[2], detail: msg[3] || "" }); + } + }); + ws.on("error", (e) => done({ url, ok: false, detail: "ws error: " + e.message })); + }); + + const results = await Promise.all(relays.map(publishOne)); + const accepted = results.filter((r) => r.ok).length; + return hOk({ recipientPkHex, accepted, total: results.length, results }); + } catch (e) { return hErr(e); } +}); + +function openHermesWindow() { + if (hermesWin && !hermesWin.isDestroyed()) { + hermesWin.focus(); return; + } + hermesWin = new BrowserWindow({ + width: 720, height: 620, title: "Messages — Theseus", + backgroundColor: "#0b0e14", + webPreferences: { + preload: path.join(__dirname, "messages-preload.js"), + contextIsolation: true, nodeIntegration: false, + }, + }); + hermesWin.setMenuBarVisibility(false); + hermesWin.loadFile("messages.html"); + hermesWin.on("closed", () => { hermesWin = null; }); +} + +ipcMain.handle("hermes-open", () => { openHermesWindow(); return { ok: true }; }); + +// Ctrl+Shift+M anywhere in Theseus opens (or focuses) the Messages panel. +// One app-level hook: no per-window wiring, no per-tab plumbing, matches +// however many WebContents Theseus ends up owning. +app.on("web-contents-created", (_event, wc) => { + wc.on("before-input-event", (e, input) => { + if (input.type !== "keyDown") return; + if (input.control && input.shift && (input.key === "M" || input.key === "m")) { + openHermesWindow(); + e.preventDefault(); + } + }); +}); + // THESEUS_NO_AUTOSTART lets a test harness reuse serveBns/resolveHost without // launching the full UI (see dev/selftest.js). Normal `npm start` is unchanged. if (!process.env.THESEUS_NO_AUTOSTART) { diff --git a/messages-preload.js b/messages-preload.js new file mode 100644 index 0000000..07ddfc2 --- /dev/null +++ b/messages-preload.js @@ -0,0 +1,23 @@ +// Renderer bridge for the Messages panel. +// All hermes-* IPC returns { ok, ... } | { ok: false, err }. +const { contextBridge, ipcRenderer } = require("electron"); + +contextBridge.exposeInMainWorld("hermes", { + status: () => ipcRenderer.invoke("hermes-status"), + init: (mnemonic) => ipcRenderer.invoke("hermes-init", { mnemonic }), + close: () => ipcRenderer.invoke("hermes-close"), + inbox: () => ipcRenderer.invoke("hermes-inbox"), + send: (to, text) => ipcRenderer.invoke("hermes-send", { to, text }), + // Server-push: new decrypted message arrived. Callback gets { senderPkHex, senderName, text, createdAt }. + onMessage: (cb) => { + const wrapped = (_e, msg) => cb(msg); + ipcRenderer.on("hermes-message", wrapped); + return () => ipcRenderer.removeListener("hermes-message", wrapped); + }, + // Server-push: relay connection state changed. Callback gets { relaysConnected, relaysTotal }. + onStatus: (cb) => { + const wrapped = (_e, s) => cb(s); + ipcRenderer.on("hermes-status-update", wrapped); + return () => ipcRenderer.removeListener("hermes-status-update", wrapped); + }, +}); diff --git a/messages.html b/messages.html new file mode 100644 index 0000000..3524b7f --- /dev/null +++ b/messages.html @@ -0,0 +1,206 @@ + + + + +Messages — Theseus + + + + +
+ ⛓️✉️ +

Hermes Messages

+ connecting… + +
+ +
+ +
+

Enter your wallet mnemonic. Hermes derives its Nostr identity at + silentmode/messenger/0 — a purpose disjoint from your + wallet's transaction key, so a compromised relay never yields tx-signing power. + Nothing is stored on disk; close the panel and the identity is dropped.

+
This is a proof build. Use a wallet you don't hold funds in.
+ +
+ +
+
+
+ +
+
no messages yet — subscribed to inbox…
+
+
+ + + +
+
+
+
+ +
+ + + + diff --git a/package-lock.json b/package-lock.json index 561b80f..858b87a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "theseus-navigator", - "version": "0.0.1", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "theseus-navigator", - "version": "0.0.1", + "version": "0.0.3", "dependencies": { "fetch-socks": "^1.3.3", + "nostr-tools": "^2.10.4", "socks-proxy-agent": "^10.1.0", "ws": "^8.18.0" }, @@ -591,6 +592,45 @@ "node": ">= 10.0.0" } }, + "node_modules/@noble/ciphers": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@npmcli/fs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", @@ -644,6 +684,42 @@ "node": ">=14" } }, + "node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", + "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz", + "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -4053,6 +4129,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nostr-tools": { + "version": "2.24.2", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.24.2.tgz", + "integrity": "sha512-nVf8tvsDPsZvvtT5csRTrMrdrhTId8wl3IgDHt1BWCuL6J+IlmBOW68SCl7SQ6K0PoxenpLPHSKLo4yj49JybA==", + "license": "Unlicense", + "dependencies": { + "@noble/ciphers": "2.1.1", + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0", + "@scure/bip32": "2.0.1", + "@scure/bip39": "2.0.1", + "nostr-wasm": "0.1.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/nostr-wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", + "license": "MIT" + }, "node_modules/npmlog": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", @@ -5021,7 +5126,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 5faa9a4..1f496e6 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "fetch-socks": "^1.3.3", + "nostr-tools": "^2.10.4", "socks-proxy-agent": "^10.1.0", "ws": "^8.18.0" }, @@ -45,6 +46,9 @@ "home-preload.js", "collision.html", "collision-preload.js", + "messages.html", + "messages-preload.js", + "lib/**/*", "package.json", "node_modules/**/*", "!**/*.md", @@ -57,7 +61,8 @@ "extraResources": [ { "from": "tor", "to": "tor" }, { "from": "../Argus/src/lib/resolver-web.js", "to": "resolver-web.mjs" }, - { "from": "../Argus/src/lib/password-vault.js", "to": "password-vault.mjs" } + { "from": "../Argus/src/lib/password-vault.js", "to": "password-vault.mjs" }, + { "from": "lib/hermes.js", "to": "lib/hermes.mjs" } ], "win": { "target": ["nsis", "portable"] }, "nsis": {