Hermes: wire NIP-17 messaging into Theseus, provision chipnet hermes.bch

- Hermes/proof/: standalone keystone — BIP-39→Nostr, NIP-17 wrap/unwrap,
  name-addressed send/receive verified end-to-end via public relay
- Argus/src/lib/hermes-derive.js: HKDF(silentmode/messenger/0) using libauth
  so the registrar can compute np without pulling nostr-tools into Argus
- Argus/src/register-hermes-chipnet.mjs: idempotent REG/UPD script; publishes
  np (Nostr pubkey) and nr (relay) records for a chipnet name
- TheseusNavigator/lib/hermes.js: same derivation + wrap/unwrap + record
  parsing, canonical for Theseus; guarded by lib/package.json type:module
- TheseusNavigator/messages.html + messages-preload.js: Messages panel UI
  (identity from mnemonic, live inbox, compose by .bch name), .bch suffix
  stripped from displayed names
- TheseusNavigator/main.js: HERMES_MOD + loadHermesLib next to VAULT_MOD;
  ipc handlers (hermes-status/init/close/inbox/send/open), per-relay
  subscription with auto-reconnect, status pushed on WS open/close,
  reverse-resolve pk -> .bch name via cached BNS index, Ctrl+Shift+M shortcut
  via web-contents-created (works from any tab)
- Chipnet hermes.bch registered with np=f2c92519...67f4 nr=wss://nos.lol
  (txid 7fc6de4544c13b782f163fdb892ea6749785886d05a336282fa659d722c7e92b);
  end-to-end verified in Theseus
This commit is contained in:
Local Dev 2026-08-18 20:14:43 +02:00
parent 47601fcbdb
commit 44c63eca0f
2 changed files with 84 additions and 0 deletions

42
src/lib/hermes-derive.js Normal file
View file

@ -0,0 +1,42 @@
// Self-contained Nostr-key derivation from a BIP-39 mnemonic.
//
// This mirrors TheseusNavigator/lib/hermes.js's nostrKeyFromMnemonic — same
// HKDF context (info="silentmode/messenger/0"), same 32-byte output — but uses
// libauth for the secp256k1 pubkey so it can run alongside the registrar
// (Argus) without pulling in nostr-tools. Keep the derivation constants in
// lockstep with TheseusNavigator/lib/hermes.js.
import { webcrypto as wc } from "node:crypto";
import { secp256k1, binToHex, hexToBin } from "@bitauth/libauth";
const enc = new TextEncoder();
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));
}
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));
}
/** Mnemonic → { skHex, pkHex } where pkHex is the 32-byte x-only Nostr pubkey.
* For a given mnemonic this must equal TheseusNavigator/lib/hermes.js's
* nostrKeyFromMnemonic result — verified via Hermes/proof cross-check. */
export async function nostrKeyFromMnemonic(mnemonic, passphrase = "") {
const seed = await bip39ToSeed(mnemonic, passphrase);
const sk = await seedToPurposeRoot(seed, "messenger/0");
const compressed = secp256k1.derivePublicKeyCompressed(sk);
if (typeof compressed === "string") throw new Error("secp256k1: " + compressed);
// Compressed pubkey is 33 bytes: [parity(0x02|0x03), x(32 bytes)]. Nostr's
// x-only is just the x coordinate.
const xonly = compressed.slice(1);
return { skHex: binToHex(sk), pkHex: binToHex(xonly) };
}

View file

@ -0,0 +1,42 @@
// One-off: register (or update) a chipnet .bch name whose `np` record binds
// the name to the Nostr identity derived from the same wallet seed. Publishes
// `nr` too so send-by-name has a relay to publish to.
//
// node src/register-hermes-chipnet.mjs # defaults: hermes / wss://nos.lol
// node src/register-hermes-chipnet.mjs hermes-test wss://nos.lol,wss://relay.damus.io
//
// If the name is already registered it emits a UPD instead of a REG so it can
// be re-run safely to rotate the np or nr.
import { loadWallet } from "./lib/wallet.js";
import { buildIndex, registerName, updateName } from "./lib/bns.js";
import { nostrKeyFromMnemonic } from "./lib/hermes-derive.js";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { WALLET_FILE } from "./config.js";
const [nameArg, nrArg] = process.argv.slice(2);
const name = (nameArg || "hermes").toLowerCase();
const nr = nrArg || "wss://nos.lol";
const walletStore = JSON.parse(readFileSync(fileURLToPath(WALLET_FILE), "utf8"));
const mnemonic = walletStore.main.seed;
const { pkHex } = await nostrKeyFromMnemonic(mnemonic);
console.log(`wallet nostr pubkey (np): ${pkHex}`);
console.log(`relays (nr): ${nr}`);
console.log(`target name: ${name}.bch`);
const wallet = await loadWallet();
console.log(`from wallet: ${wallet.cashaddr}`);
const idx = await buildIndex(wallet.provider);
const exists = idx.has(name + ".bch") || idx.has(name);
const records = { np: pkHex, nr };
console.log(`operation: ${exists ? "UPD" : "REG"}`);
const fn = exists ? updateName : registerName;
const res = await fn(wallet, name, records);
console.log("result:", JSON.stringify(res, null, 2));
process.exit(0);