Start a new conversation
-
+
@@ -254,15 +263,32 @@ const err = (el, m) => { el.textContent = m || ""; };
const state = {
ready: false,
- me: null, // { pkHex, npub }
+ me: null, // { pkHex, npub, handle? }
canUseVault: false,
contacts: [],
messages: [], // flat inbox from host
readState: {}, // peerPkHex → epoch sec read-up-to
selected: null, // peerPkHex (lowercase) of focused thread
threads: new Map(), // peerPkHex → { peerName, messages: [...] }
+ renderMode: "scheme", // "scheme" (hermes:alice) | "dotted" | "email"
};
+// Must stay in sync with HERMES_TLD / HERMES_EMAIL_PARENT in lib/hermes.js —
+// used only for display; actual resolution happens host-side via parseHandle.
+const HERMES_TLD = "erm";
+const HERMES_EMAIL_PARENT = "hermes.tel";
+function renderHandle(name, mode) {
+ const m = mode || state.renderMode;
+ const n = String(name || "").toLowerCase();
+ if (!n || !n.includes(".")) return n;
+ const suffix = "." + HERMES_TLD;
+ if (!n.endsWith(suffix)) return n;
+ const local = n.slice(0, -suffix.length);
+ if (m === "dotted") return n;
+ if (m === "email") return `${local}@${HERMES_EMAIL_PARENT}`;
+ return `hermes:${local}`;
+}
+
function h(tag, attrs, kids) {
const n = document.createElement(tag);
if (attrs) for (const k of Object.keys(attrs)) {
@@ -285,7 +311,7 @@ function initials(name) {
function groupByPeer(messages, contacts, mePk) {
const nameOf = new Map();
- for (const c of contacts) nameOf.set(c.pkHex, c.nickname || c.name);
+ for (const c of contacts) nameOf.set(c.pkHex, c.nickname || renderHandle(c.name));
const map = new Map();
for (const m of messages) {
const peer = m.peerPkHex || (m.direction === "out" ? null : m.senderPkHex);
@@ -327,10 +353,10 @@ function renderContacts() {
list.textContent = "";
const q = $("search").value.trim().toLowerCase();
const nameOf = new Map();
- for (const c of state.contacts) nameOf.set(c.pkHex, c.nickname || c.name);
+ for (const c of state.contacts) nameOf.set(c.pkHex, c.nickname || renderHandle(c.name));
const items = [];
for (const [pk, t] of state.threads) {
- const label = t.peerName || nameOf.get(pk) || pk.slice(0, 10) + "…";
+ const label = (t.peerName && renderHandle(t.peerName)) || nameOf.get(pk) || pk.slice(0, 10) + "…";
if (q && !label.toLowerCase().includes(q) && !pk.includes(q)) continue;
const last = t.messages[t.messages.length - 1];
items.push({ pk, label, last, unread: unreadCount(t, state.readState[pk]) });
@@ -338,7 +364,7 @@ function renderContacts() {
// Contacts with no messages yet
for (const c of state.contacts) {
if (state.threads.has(c.pkHex)) continue;
- const label = c.nickname || c.name || c.pkHex.slice(0, 10) + "…";
+ const label = c.nickname || renderHandle(c.name) || c.pkHex.slice(0, 10) + "…";
if (q && !label.toLowerCase().includes(q) && !c.pkHex.includes(q)) continue;
items.push({ pk: c.pkHex, label, last: null, unread: 0 });
}
@@ -379,8 +405,8 @@ function renderThread() {
}
const t = state.threads.get(pk);
const nameOf = new Map();
- for (const c of state.contacts) nameOf.set(c.pkHex, c.nickname || c.name);
- const label = (t && t.peerName) || nameOf.get(pk) || pk.slice(0, 16) + "…";
+ for (const c of state.contacts) nameOf.set(c.pkHex, c.nickname || renderHandle(c.name));
+ const label = (t && t.peerName && renderHandle(t.peerName)) || nameOf.get(pk) || pk.slice(0, 16) + "…";
$("threadName").textContent = label;
$("threadPk").textContent = pk;
head.style.display = "flex";
@@ -458,10 +484,33 @@ function showApp(me) {
state.me = me;
$("signin").classList.remove("active");
$("app").classList.add("active");
+ const label = me && me.handle ? renderHandle(me.handle) :
+ me && me.npub ? me.npub.slice(0, 14) + "…" : "—";
+ $("whoBtn").textContent = "Signed in as " + label;
+ $("menuBtn").style.display = "grid";
refreshContacts().then(refreshInbox);
refreshStatus();
}
+function showSignin() {
+ state.ready = false;
+ state.me = null;
+ state.selected = null;
+ state.messages = [];
+ state.threads = new Map();
+ $("app").classList.remove("active", "show-thread");
+ $("signin").classList.add("active");
+ $("menuBtn").style.display = "none";
+ closeMenu();
+}
+
+function openMenu() { $("menu").style.display = "block"; }
+function closeMenu() { $("menu").style.display = "none"; }
+document.addEventListener("click", (e) => {
+ const m = $("menu"); if (!m) return;
+ if (m.style.display === "block" && !m.contains(e.target) && e.target !== $("menuBtn")) closeMenu();
+});
+
async function initWithOpts(opts) {
err($("signinErr"), "");
const btn = opts.useVault ? $("useVaultBtn") : $("useMnemonicBtn");
@@ -508,6 +557,12 @@ async function initWithOpts(opts) {
if (window.silentmode.on) {
window.silentmode.on("hermes-message", () => refreshInbox());
window.silentmode.on("hermes-status-update", (p) => setStatus(p));
+ // Auto-restore fires on app launch if the user had a saved identity.
+ // The host already set hermesState up; the panel just needs to react
+ // as if it finished a signin.
+ window.silentmode.on("hermes-restored", (p) => {
+ if (!state.ready && p && p.pkHex) showApp({ pkHex: p.pkHex, npub: p.npub, handle: p.handle || null });
+ });
}
// Mild polling in case the event bus misses a tick (dev launcher, re-attach).
@@ -525,6 +580,16 @@ $("useMnemonicBtn").addEventListener("click", () => {
initWithOpts({ useVault: false, mnemonic });
});
$("popOutBtn").addEventListener("click", () => bridge.invoke("open-messages"));
+$("menuBtn").addEventListener("click", (e) => {
+ e.stopPropagation();
+ if ($("menu").style.display === "block") closeMenu();
+ else openMenu();
+});
+$("signOutBtn").addEventListener("click", async () => {
+ closeMenu();
+ try { await bridge.invoke("signOut"); } catch {}
+ showSignin();
+});
$("search").addEventListener("input", renderContacts);
$("backBtn").addEventListener("click", () => {
diff --git a/lib/hermes.js b/lib/hermes.js
index 769a07d3..64e157b5 100644
--- a/lib/hermes.js
+++ b/lib/hermes.js
@@ -79,6 +79,92 @@ export function nostrKeyFromRoot(root) {
const HEX_PUBKEY_RE = /^[0-9a-f]{64}$/i;
+// ---- handle forms ----------------------------------------------------------
+// Hermes accepts three equivalent handle forms, all resolving to the same
+// BNS registration under the hood:
+//
+// hermes:alice — URI scheme, hides the TLD (default display)
+// alice.erm — dotted BNS form
+// alice@hermes.tel — email form; `hermes.tel` is a BNS name whose
+// signed-records manifest lists sub-identities
+//
+// `.erm` and `hermes.tel` are the planned on-chain names; both mint on
+// mainnet, pending user action. Until then, parseHandle still accepts all
+// three forms — resolution against the real chain just returns "not found"
+// for unminted TLDs/parents. Existing `.bch` names keep working untouched.
+export const HERMES_SCHEME = "hermes";
+export const HERMES_TLD = "erm";
+export const HERMES_EMAIL_PARENT = "hermes.tel";
+const HANDLE_LOCAL_RE = /^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
+const DOTTED_NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/;
+
+/**
+ * Normalize any accepted handle form to a canonical lookup descriptor:
+ * { kind: "hex", value: "<64 hex>" }
+ * { kind: "npub", value: "npub1…" }
+ * { kind: "name", value: "alice.erm" } — BNS name lookup
+ * { kind: "sub", value: { local: "alice", parent: "hermes.tel" } }
+ * — signed-records
+ * sub-identity lookup
+ */
+export function parseHandle(input) {
+ const s = String(input || "").trim();
+ if (!s) throw new Error("empty handle");
+ if (/^[0-9a-f]{64}$/i.test(s)) return { kind: "hex", value: s.toLowerCase() };
+ if (s.toLowerCase().startsWith("npub1")) return { kind: "npub", value: s };
+ const lower = s.toLowerCase();
+
+ // hermes:alice — expand to the default TLD. `.erm` is the planned name,
+ // but any TLD switch is a one-line change to HERMES_TLD above.
+ const schemePrefix = HERMES_SCHEME + ":";
+ if (lower.startsWith(schemePrefix)) {
+ const local = lower.slice(schemePrefix.length).trim();
+ if (!HANDLE_LOCAL_RE.test(local)) throw new Error(`bad hermes: handle: ${s.slice(0, 48)}`);
+ return { kind: "name", value: `${local}.${HERMES_TLD}` };
+ }
+
+ // alice@parent.tld — email form, treated as a sub-identity under parent.
+ if (lower.includes("@")) {
+ const at = lower.indexOf("@");
+ const local = lower.slice(0, at);
+ const parent = lower.slice(at + 1);
+ if (!HANDLE_LOCAL_RE.test(local) || !DOTTED_NAME_RE.test(parent)) {
+ throw new Error(`bad email handle: ${s.slice(0, 48)}`);
+ }
+ return { kind: "sub", value: { local, parent } };
+ }
+
+ // alice.erm, alice.bch, alice.tel — dotted BNS name. Must have at least
+ // one dot to disambiguate from "bare" sub-identity requests (which the
+ // client enriches with the default parent before calling in).
+ if (DOTTED_NAME_RE.test(lower)) {
+ return { kind: "name", value: lower };
+ }
+
+ throw new Error(`unrecognised handle: ${s.slice(0, 48)}`);
+}
+
+/**
+ * Render a canonical descriptor (or a BNS name) in the user's preferred form.
+ * Modes:
+ * "scheme" (default) → hermes:alice (only when the name is under HERMES_TLD)
+ * "dotted" → alice.erm
+ * "email" → alice@hermes.tel (only for HERMES_TLD names)
+ * Non-hermes names (alice.bch, bob.sc) always render dotted — the scheme
+ * and email forms are reserved for the Hermes TLD so a .bch name can't
+ * accidentally look like a sub-identity.
+ */
+export function renderHandle(name, mode = "scheme") {
+ const n = String(name || "").toLowerCase();
+ if (!n || !n.includes(".")) return n;
+ const suffix = "." + HERMES_TLD;
+ if (!n.endsWith(suffix)) return n;
+ const local = n.slice(0, -suffix.length);
+ if (mode === "dotted") return n;
+ if (mode === "email") return `${local}@${HERMES_EMAIL_PARENT}`;
+ return `${HERMES_SCHEME}:${local}`;
+}
+
export function parseNpRecord(raw) {
if (typeof raw !== "string" || !raw.trim()) throw new Error("np: empty or missing");
const v = raw.trim();
diff --git a/main.js b/main.js
index b0ea2ac8..f422aa0e 100644
--- a/main.js
+++ b/main.js
@@ -9776,6 +9776,140 @@ let hermesWin = null;
// (nostr-tools expects Uint8Array; converting on demand keeps the surface easier to reason about).
let hermesState = null;
+// ---- handle resolution -----------------------------------------------------
+// One shared helper for every "turn an input into a Hermes recipient" callsite
+// (contactsAdd, send, and their standalone-window ipcMain twins). Takes any
+// form the client accepts — direct hex, npub, hermes:alice, alice.erm,
+// alice.bch, alice@parent.tld — and returns:
+// { pkHex, relays: string[], canonicalName: string | null }
+// `canonicalName` is set when the input resolved via a BNS name (used for the
+// contacts record); null for direct-key forms. `relays` falls back to defaults
+// when the resolved entry has no nr record.
+//
+// Sub-identity (alice@parent.tld) resolution: the parent's signed-records
+// manifest will carry sub-identities once the mainnet mint of hermes.erm /
+// hermes.tel lands. Until then, every sub lookup returns "name not registered"
+// — but the client-side parser accepts the form, so the UI already shows the
+// right affordance.
+async function hermesResolveTo(input, { defaultRelays } = {}) {
+ const H = await loadHermesLib();
+ const parsed = H.parseHandle(input);
+ if (parsed.kind === "hex") {
+ return { pkHex: parsed.value, relays: (defaultRelays || HERMES_DEFAULT_RELAYS).slice(), canonicalName: null };
+ }
+ if (parsed.kind === "npub") {
+ return { pkHex: H.parseNpRecord(parsed.value), relays: (defaultRelays || HERMES_DEFAULT_RELAYS).slice(), canonicalName: null };
+ }
+ if (parsed.kind === "name") {
+ const R = await getResolver();
+ const canonicalName = R.normalizeName(parsed.value);
+ const entry = await R.resolveName(canonicalName, { WebSocket });
+ if (!entry) throw new Error(`no on-chain registration for ${canonicalName}`);
+ const info = H.parseHermesRecords(entry, { defaultRelays: defaultRelays || HERMES_DEFAULT_RELAYS });
+ return { pkHex: info.npPk, relays: info.relays, canonicalName };
+ }
+ if (parsed.kind === "sub") {
+ // alice@hermes.tel style. Needs the parent's signed-records manifest to
+ // ship — pending mainnet. For now, give the client a crisp error so the
+ // panel can surface "not available yet" rather than looking generic.
+ throw new Error(
+ `sub-identity "${parsed.value.local}@${parsed.value.parent}" lookup not available yet ` +
+ "(waiting on mainnet mint of the parent name's signed-records manifest)");
+ }
+ throw new Error("could not resolve handle");
+}
+
+// ---- identity persistence --------------------------------------------------
+// Save-forever signin. Theseus-side: safeStorage (DPAPI on Windows, Keychain
+// on macOS, libsecret on Linux) wraps the raw skHex at rest. The accompanying
+// metadata (handle label, source, savedAt) is plaintext — nothing in it
+// unlocks the key. One active identity per profile; a second successful init
+// with persist:true replaces whatever was there. Signing out erases both the
+// sealed blob and the metadata.
+const hermesIdentityDir = () => path.join(app.getPath("userData"), "hermes-identity");
+const hermesSealedKeyPath = () => path.join(hermesIdentityDir(), "id.sealed");
+const hermesIdentityMetaPath = () => path.join(hermesIdentityDir(), "id.json");
+
+function hermesSaveIdentity({ skHex, pkHex, npub, source, handle }) {
+ if (!safeStorage || !safeStorage.isEncryptionAvailable()) {
+ console.warn("[hermes] safeStorage unavailable — identity not persisted");
+ return false;
+ }
+ try {
+ hermesEnsureDir(hermesIdentityDir());
+ const sealed = safeStorage.encryptString(String(skHex));
+ fs.writeFileSync(hermesSealedKeyPath() + ".tmp", sealed);
+ fs.renameSync(hermesSealedKeyPath() + ".tmp", hermesSealedKeyPath());
+ const meta = { pkHex, npub, source, handle: handle || null, savedAt: new Date().toISOString() };
+ fs.writeFileSync(hermesIdentityMetaPath() + ".tmp", JSON.stringify(meta, null, 2));
+ fs.renameSync(hermesIdentityMetaPath() + ".tmp", hermesIdentityMetaPath());
+ return true;
+ } catch (e) {
+ console.error("[hermes] identity save failed:", e.message);
+ return false;
+ }
+}
+
+function hermesLoadIdentity() {
+ try {
+ if (!fs.existsSync(hermesSealedKeyPath()) || !fs.existsSync(hermesIdentityMetaPath())) return null;
+ if (!safeStorage || !safeStorage.isEncryptionAvailable()) return null;
+ const sealed = fs.readFileSync(hermesSealedKeyPath());
+ const skHex = safeStorage.decryptString(sealed);
+ const meta = JSON.parse(fs.readFileSync(hermesIdentityMetaPath(), "utf8"));
+ if (!/^[0-9a-f]{64}$/i.test(skHex || "") || !meta || !meta.pkHex) return null;
+ return { skHex, ...meta };
+ } catch (e) {
+ console.error("[hermes] identity load failed:", e.message);
+ return null;
+ }
+}
+
+function hermesForgetIdentity() {
+ for (const p of [hermesSealedKeyPath(), hermesIdentityMetaPath()]) {
+ try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch {}
+ }
+}
+
+// Called once at startup. If a saved identity exists, bring hermesState up
+// without the user re-signing. Keeps the panel/window ready-to-use on launch
+// so inbox watchers reconnect without a UI gesture. Non-fatal — any failure
+// (safeStorage locked, file corrupt, lib load crash) silently falls back to
+// the signin screen as if nothing was saved.
+async function hermesAutoRestore() {
+ try {
+ if (hermesState) return; // already live (dev-restart edge case)
+ const saved = hermesLoadIdentity();
+ if (!saved) return;
+ const H = await loadHermesLib();
+ const sk = Uint8Array.from(Buffer.from(saved.skHex, "hex"));
+ const pkHex = saved.pkHex;
+ const npub = saved.npub;
+ const persisted = hermesLoadInboxFromDisk(pkHex);
+ const seen = new Set();
+ for (const m of persisted) seen.add((m.senderPkHex || "") + "\0" + m.createdAt + "\0" + m.text);
+ hermesState = {
+ skHex: saved.skHex, pkHex, npub,
+ inbox: persisted,
+ readState: hermesLoadReadState(pkHex),
+ relays: new Map(), _seen: seen,
+ source: saved.source || "restored",
+ handle: saved.handle || null,
+ };
+ void H; // ensure lib is warm so later send/wrap paths don't block
+ for (const url of HERMES_DEFAULT_RELAYS) {
+ hermesState.relays.set(url, await hermesConnectRelay(url));
+ }
+ console.log("[hermes] identity restored for", pkHex.slice(0, 10) + "…", "source=" + hermesState.source);
+ hermesEmit("hermes-restored", { pkHex, npub, source: hermesState.source });
+ } catch (e) {
+ console.error("[hermes] auto-restore failed:", e.message);
+ }
+}
+// Fire a little after app ready so loadHermesLib's dynamic import and
+// safeStorage both exist. Non-blocking.
+app.whenReady().then(() => setTimeout(() => { void hermesAutoRestore(); }, 1500));
+
const hOk = (o = {}) => ({ ok: true, ...o });
const hErr = (e) => ({ ok: false, err: String((e && e.message) || e) });
@@ -10012,23 +10146,12 @@ ipcMain.handle("hermes-contacts-list", () => hOk({ contacts: hermesLoadContacts(
ipcMain.handle("hermes-contacts-add", async (_e, { name, nickname } = {}) => {
const raw = String(name || "").trim();
- if (!raw) return hErr("name is required (e.g. alice.bch or an npub)");
- // Resolve to a pkHex: direct hex, npub, or .bch name via on-chain np.
- let pkHex = null, canonicalName = raw;
+ if (!raw) return hErr("name is required (e.g. hermes:alice or an npub)");
+ let pkHex, canonicalName;
try {
- const H = await loadHermesLib();
- if (/^[0-9a-f]{64}$/i.test(raw)) {
- pkHex = raw.toLowerCase();
- } else if (raw.startsWith("npub1")) {
- pkHex = H.parseNpRecord(raw);
- } else {
- const R = await getResolver();
- canonicalName = R.normalizeName(raw);
- const entry = await R.resolveName(canonicalName, { WebSocket });
- if (!entry) return hErr(`no on-chain registration for ${canonicalName}`);
- const parsed = H.parseHermesRecords(entry);
- pkHex = parsed.npPk;
- }
+ const r = await hermesResolveTo(raw);
+ pkHex = r.pkHex;
+ canonicalName = r.canonicalName || raw;
} catch (e) { return hErr(e); }
const contacts = hermesLoadContacts();
const existing = contacts.findIndex((c) => c.pkHex === pkHex);
@@ -10167,27 +10290,30 @@ global._hermesAPI = {
try {
hermesTeardown();
const H = await loadHermesLib();
- let sk, pkHex, npub;
+ let sk, pkHex, npub, source;
if (opts.useVault) {
if (!vaultState || !vaultState.messengerRoot) {
return hErr("password vault is locked or was set up without a mnemonic");
}
({ sk, pkHex, npub } = H.nostrKeyFromRoot(vaultState.messengerRoot));
+ source = "vault";
} else {
const mnemonic = opts.mnemonic;
if (!mnemonic || typeof mnemonic !== "string" || mnemonic.trim().split(/\s+/).length < 12) {
return hErr("enter a 12- or 24-word mnemonic");
}
({ sk, pkHex, npub } = await H.nostrKeyFromMnemonic(mnemonic.trim()));
+ source = "mnemonic";
}
+ const skHex = Buffer.from(sk).toString("hex");
const persisted = hermesLoadInboxFromDisk(pkHex);
const seen = new Set();
for (const m of persisted) seen.add((m.senderPkHex || "") + "\0" + m.createdAt + "\0" + m.text);
hermesState = {
- skHex: Buffer.from(sk).toString("hex"),
- pkHex, npub, inbox: persisted,
+ skHex, pkHex, npub, inbox: persisted,
readState: hermesLoadReadState(pkHex),
relays: new Map(), _seen: seen,
+ source, handle: opts.handle || null,
};
for (const url of HERMES_DEFAULT_RELAYS) {
hermesState.relays.set(url, await hermesConnectRelay(url));
@@ -10195,10 +10321,25 @@ global._hermesAPI = {
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, source: opts.useVault ? "vault" : "mnemonic",
- relaysConnected: connected, relaysTotal: hermesState.relays.size });
+ // Persist by default. Caller passes `remember: false` for a one-shot
+ // signin that disappears when hermesTeardown runs. The vault path only
+ // persists if the user said so — vault-wrapped identities are
+ // re-derivable from the already-unlocked vault, so saving the sealed
+ // skHex a second time is write-amplification, not security.
+ const remember = opts.remember !== false;
+ if (remember) {
+ hermesSaveIdentity({ skHex, pkHex, npub, source, handle: opts.handle || null });
+ }
+ return hOk({ npub, pkHex, source,
+ relaysConnected: connected, relaysTotal: hermesState.relays.size,
+ persisted: remember });
} catch (e) { hermesTeardown(); return hErr(e); }
},
+ signOut: () => {
+ hermesForgetIdentity();
+ hermesTeardown();
+ return hOk({ signedOut: true });
+ },
markThreadRead: ({ peerPkHex, upToCreatedAt } = {}) => {
if (!hermesState) return hErr("not initialised");
if (!peerPkHex || typeof peerPkHex !== "string") return hErr("peerPkHex required");
@@ -10213,19 +10354,12 @@ global._hermesAPI = {
contactsList: () => hOk({ contacts: hermesLoadContacts().slice() }),
contactsAdd: async ({ name, nickname } = {}) => {
const raw = String(name || "").trim();
- if (!raw) return hErr("name is required (e.g. alice.bch or an npub)");
- let pkHex = null, canonicalName = raw;
+ if (!raw) return hErr("name is required (e.g. hermes:alice or an npub)");
+ let pkHex, canonicalName;
try {
- const H = await loadHermesLib();
- if (/^[0-9a-f]{64}$/i.test(raw)) pkHex = raw.toLowerCase();
- else if (raw.startsWith("npub1")) pkHex = H.parseNpRecord(raw);
- else {
- const R = await getResolver();
- canonicalName = R.normalizeName(raw);
- const entry = await R.resolveName(canonicalName, { WebSocket });
- if (!entry) return hErr(`no on-chain registration for ${canonicalName}`);
- pkHex = H.parseHermesRecords(entry).npPk;
- }
+ const r = await hermesResolveTo(raw);
+ pkHex = r.pkHex;
+ canonicalName = r.canonicalName || raw;
} catch (e) { return hErr(e); }
const contacts = hermesLoadContacts();
const existing = contacts.findIndex((c) => c.pkHex === pkHex);
@@ -10260,19 +10394,12 @@ global._hermesAPI = {
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;
- }
+ let recipientPkHex, relays;
+ try {
+ const r = await hermesResolveTo(to, { defaultRelays: HERMES_DEFAULT_RELAYS });
+ recipientPkHex = r.pkHex;
+ relays = r.relays;
+ } catch (e) { return hErr(e); }
const sk = Buffer.from(hermesState.skHex, "hex");
const wrap = H.wrapChat({ senderSk: sk, recipientPkHex, text });
const publishOne = (url) => new Promise((resolve) => {
@@ -10313,21 +10440,12 @@ ipcMain.handle("hermes-send", async (_e, { to, text } = {}) => {
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;
- }
+ let recipientPkHex, relays;
+ try {
+ const r = await hermesResolveTo(to, { defaultRelays: HERMES_DEFAULT_RELAYS });
+ recipientPkHex = r.pkHex;
+ relays = r.relays;
+ } catch (e) { return hErr(e); }
const sk = Buffer.from(hermesState.skHex, "hex");
const wrap = H.wrapChat({ senderSk: sk, recipientPkHex, text });