From b775d2b29bcc2964cbc374771f86ce39d83cb8b7 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 30 Aug 2026 23:01:26 -0700 Subject: Google accounts can speak: the DM seal signs with the present for remote signers (the pomegranate FROST operators refuse backdated events, and NIP-59's tweak on the seal protects nothing — only the recipient ever sees that layer; the public-facing wrap keeps its tweak under our own ephemeral key), and the registrar welcomes each PERSON once instead of once per key — the Google flow's two claims (wallet key, then real npub with the wallet as manager) were earning the same inbox two identical greetings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr --- names/server.js | 14 ++++++++++---- src/dm.js | 9 ++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/names/server.js b/names/server.js index c0b3c1f..55c32ae 100644 --- a/names/server.js +++ b/names/server.js @@ -158,12 +158,18 @@ async function inboxRelaysOf(pubkey) { return []; } -async function sendWelcome(pubkey) { +async function sendWelcome(pubkey, linkedKeys = []) { if (!WELCOME) return; state.welcomed ||= {}; - if (state.welcomed[pubkey]) return; - state.welcomed[pubkey] = Date.now(); + // One welcome per PERSON, not per key: a Google sign-up claims twice — + // the wallet key takes the placeholder name, then the real npub takes + // its own with the wallet key as manager — and greeting each pubkey put + // two identical DMs in the same inbox. + const keys = [pubkey, ...linkedKeys].filter((k) => /^[0-9a-f]{64}$/.test(k || '')); + const seen = keys.some((k) => state.welcomed[k]); + for (const k of keys) state.welcomed[k] ||= Date.now(); persist(); + if (seen) return; try { // wrapManyEvents returns [sender-copy, recipient] — publish ONLY the // recipient's wrap. A sender copy per registrant flooded Adam's own @@ -1220,7 +1226,7 @@ Bun.serve({ }; persist(); log(`${existing ? 'updated' : 'registered'} ${key} for ${auth.pubkey.slice(0, 12)}`); - if (!existing) sendWelcome(state.names[key].pubkey).catch(() => {}); + if (!existing) sendWelcome(state.names[key].pubkey, [state.names[key].manager]).catch(() => {}); return json({ ok: true, name, address: key, record: recordName(name, domain) }); } diff --git a/src/dm.js b/src/dm.js index 85498f2..caf6fbf 100644 --- a/src/dm.js +++ b/src/dm.js @@ -33,7 +33,14 @@ export async function wrapDM(skOrSigner, receiverPk, rumor, extraTags = []) { kind: 13, content: await signer.encryptTo(receiverPk, JSON.stringify(rumor)), tags: [], - created_at: tweaked(), + // NIP-59 backdates the seal too, but only the recipient ever sees it + // (it rides encrypted inside the wrap) — and some remote signers, the + // pomegranate FROST operators among them, refuse to sign anything dated + // in the past ("can't sign event in past" killed every Google-account + // DM). A local key keeps the spec's letter; a remote signer signs now. + // The wrap below keeps its tweak either way — the ephemeral key that + // signs it is ours and local, and that's the layer the public sees. + created_at: skOrSigner instanceof Uint8Array ? tweaked() : now(), }); const eph = generateSecretKey(); return finalizeEvent( -- cgit v1.2.3