aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-30 23:01:26 -0700
committerAdam Soltys <[email protected]>2026-08-30 23:01:26 -0700
commitb775d2b29bcc2964cbc374771f86ce39d83cb8b7 (patch)
tree84f2be029ddc0eddfa1c8562a445e60747019224
parent280bbfd09f9bc7c948f77d4e362cc3b32d4fcfc1 (diff)
downloadcoinosv3-b775d2b29bcc2964cbc374771f86ce39d83cb8b7.tar.gz
coinosv3-b775d2b29bcc2964cbc374771f86ce39d83cb8b7.zip
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
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
-rw-r--r--names/server.js14
-rw-r--r--src/dm.js9
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(