diff options
| author | Adam Soltys <[email protected]> | 2026-08-02 13:40:54 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-02 13:40:54 -0700 |
| commit | 95263c02a13a825a8d02dbe760f2a74de8f93f77 (patch) | |
| tree | 1383d4ca48ec24eedb26fd7740f13e1295d55bdf /names/server.js | |
| parent | 6416bd5dd79c3bdde5dfa7e7ec95fc811fbcdb28 (diff) | |
| download | coinosv3-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.js | 44 |
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) }); } |
