aboutsummaryrefslogtreecommitdiffstats
path: root/names/server.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-02 13:40:54 -0700
committerAdam Soltys <[email protected]>2026-08-02 13:40:54 -0700
commit95263c02a13a825a8d02dbe760f2a74de8f93f77 (patch)
tree1383d4ca48ec24eedb26fd7740f13e1295d55bdf /names/server.js
parent6416bd5dd79c3bdde5dfa7e7ec95fc811fbcdb28 (diff)
downloadcoinosv3-95263c02a13a825a8d02dbe760f2a74de8f93f77.tar.gz
coinosv3-95263c02a13a825a8d02dbe760f2a74de8f93f77.zip
Chat v2: DMs, invites, and multi-community — coinos is the default, not the only
- NIP-17 private DMs (src/dm.js): kind-14 rumors in sender-signed seals under ephemeral wraps; interops with other nostr DM clients. Login signers gained encryptTo/decryptFrom so extensions and bunkers can seal to a peer; the inbox listens for both the login npub and the wallet key and publishes a kind-10050 DM-relay list for the wallet key when none exists. - Concord invites (CORD-05): mint shareable links (33301 bundle on relays, keys only in the URL fragment), join by pasting a link or opening v3.coinos.io/invite/…, and direct invites giftwrapped to an npub with the k-tag index hint. Bundles self-certify the founder before anything joins. - Multi-community: home view lists DM threads and communities; found your own community in-app (owner-signed genesis). coinos stays baked in as default. - Welcome DM: the names registrar DMs every first-time registrant from Adam's npub on claim (config: welcome{sk,message}; deduped in registrar state). Every new wallet auto-claims a payment address, so every new user gets one. - relay.coinos.io write policy now allows 33301/13302/13303. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'names/server.js')
-rw-r--r--names/server.js44
1 files changed, 44 insertions, 0 deletions
diff --git a/names/server.js b/names/server.js
index 57a6e8c..8a0cab3 100644
--- a/names/server.js
+++ b/names/server.js
@@ -19,6 +19,7 @@ import { readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { verifyEvent, finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure';
import * as nip44 from 'nostr-tools/nip44';
+import { wrapManyEvents } from 'nostr-tools/nip17';
import { SimplePool } from 'nostr-tools/pool';
import { npubEncode } from 'nostr-tools/nip19';
import { createHash } from 'node:crypto';
@@ -129,6 +130,48 @@ if (!state.serviceSk) { state.serviceSk = Buffer.from(generateSecretKey()).toStr
const SERVICE_SK = Uint8Array.from(Buffer.from(state.serviceSk, 'hex'));
const SERVICE_PK = getPublicKey(SERVICE_SK);
+// ---------------------------------------------------------------------------
+// Welcome DM — every first-time registrant gets a NIP-17 DM from Adam.
+// config: welcome: { sk: '<hex privkey>', message: '…' }. The registration is
+// the one moment we reliably know a new user's npub, so it doubles as the
+// "new user" signal. One per pubkey, ever (state.welcomed).
+// ---------------------------------------------------------------------------
+
+const WELCOME = CFG.welcome && /^[0-9a-f]{64}$/.test(CFG.welcome.sk || '') ? {
+ sk: Uint8Array.from(Buffer.from(CFG.welcome.sk, 'hex')),
+ message: CFG.welcome.message || 'Welcome to coinos!',
+} : null;
+
+async function inboxRelaysOf(pubkey) {
+ try {
+ const evs = await pool.querySync(LNURL_RELAYS, { kinds: [10002, 10050], authors: [pubkey] }, { maxWait: 4000 });
+ const newest = (kind) => evs.filter((e) => e.kind === kind).sort((a, b) => b.created_at - a.created_at)[0];
+ const dm = newest(10050);
+ if (dm) { const r = dm.tags.filter((t) => t[0] === 'relay' && t[1]).map((t) => t[1]); if (r.length) return r.slice(0, 4); }
+ const rl = newest(10002);
+ if (rl) return rl.tags.filter((t) => t[0] === 'r' && (!t[2] || t[2] === 'read')).map((t) => t[1]).slice(0, 4);
+ } catch {}
+ return [];
+}
+
+async function sendWelcome(pubkey) {
+ if (!WELCOME) return;
+ state.welcomed ||= {};
+ if (state.welcomed[pubkey]) return;
+ state.welcomed[pubkey] = Date.now();
+ persist();
+ try {
+ // wraps for [sender-copy, recipient], each to its owner's inbox
+ const [toSelf, toPeer] = wrapManyEvents(WELCOME.sk, [{ publicKey: pubkey }], WELCOME.message);
+ const inbox = await inboxRelaysOf(pubkey);
+ await Promise.allSettled(pool.publish([...new Set([...inbox, ...LNURL_RELAYS])], toPeer));
+ await Promise.allSettled(pool.publish(LNURL_RELAYS, toSelf));
+ log(`welcome DM sent to ${pubkey.slice(0, 12)}`);
+ } catch (e) {
+ log('welcome DM failed: ' + e.message);
+ }
+}
+
const lnurlMeta = (address) => JSON.stringify([
['text/plain', `Paying ${address}`],
['text/identifier', address],
@@ -544,6 +587,7 @@ Bun.serve({
};
persist();
log(`${existing ? 'updated' : 'registered'} ${key} for ${auth.pubkey.slice(0, 12)}`);
+ if (!existing) sendWelcome(state.names[key].pubkey).catch(() => {});
return json({ ok: true, name, address: key, record: recordName(name, domain) });
}