/** * timmy-identity.ts — Timmy's persistent Nostr identity. * * Loads a secp256k1 keypair from TIMMY_NOSTR_NSEC env var at boot. * If the env var is absent, generates an ephemeral keypair and logs * a warning instructing the operator to persist it. * * Exports `timmyIdentityService` singleton with: * .npub — bech32 public key (npub1...) * .nsec — bech32 private key (nsec1...) — never log this * .pubkeyHex — raw 64-char hex pubkey * .sign(template) — finalises and signs a Nostr event template * .encryptDm(recipientPubkeyHex, plaintext) — NIP-04 encrypt */ import { generateSecretKey, getPublicKey, finalizeEvent, nip19, nip04 } from "nostr-tools"; import type { EventTemplate, NostrEvent } from "nostr-tools"; import { makeLogger } from "./logger.js"; const logger = makeLogger("timmy-identity"); function loadOrGenerate(): { sk: Uint8Array; nsec: string; npub: string; pubkeyHex: string } { const raw = process.env["TIMMY_NOSTR_NSEC"]; if (raw && raw.startsWith("nsec1")) { try { const decoded = nip19.decode(raw); if (decoded.type !== "nsec") throw new Error("not nsec type"); const sk = decoded.data as Uint8Array; const pubkeyHex = getPublicKey(sk); const npub = nip19.npubEncode(pubkeyHex); logger.info("timmy identity loaded from env", { npub }); return { sk, nsec: raw, npub, pubkeyHex }; } catch (err) { logger.warn("TIMMY_NOSTR_NSEC is set but could not be decoded — generating ephemeral key", { error: err instanceof Error ? err.message : String(err), }); } } const sk = generateSecretKey(); const pubkeyHex = getPublicKey(sk); const nsec = nip19.nsecEncode(sk); const npub = nip19.npubEncode(pubkeyHex); logger.warn( "TIMMY_NOSTR_NSEC not set — generated ephemeral Nostr identity. " + "Set this env var permanently to preserve Timmy's identity across restarts.", { npub }, ); return { sk, nsec, npub, pubkeyHex }; } class TimmyIdentityService { readonly npub: string; readonly nsec: string; readonly pubkeyHex: string; private readonly _sk: Uint8Array; constructor() { const identity = loadOrGenerate(); this.npub = identity.npub; this.nsec = identity.nsec; this.pubkeyHex = identity.pubkeyHex; this._sk = identity.sk; } sign(template: EventTemplate): NostrEvent { return finalizeEvent(template, this._sk); } async encryptDm(recipientPubkeyHex: string, plaintext: string): Promise { return nip04.encrypt(this._sk, recipientPubkeyHex, plaintext); } } export const timmyIdentityService = new TimmyIdentityService();