/** * sprout-proxy E2E test using nostr-tools v2.23.3 * * nostr-tools is the most widely-used Nostr library in the ecosystem, * powering Coracle, Snort, Damus Web, and hundreds of other clients. * * Tests the full NIP-28 channel flow through sprout-proxy: * 1. NIP-42 authentication (automatic via onauth callback) * 2. Channel discovery (kind:40) * 3. Channel metadata (kind:41) * 4. Send a channel message (kind:42 via NIP-28 channelMessageEvent) * 5. Receive messages (kind:42 subscription) * 6. Live streaming (real-time event delivery) */ import { Relay } from 'nostr-tools/relay' import { finalizeEvent, getPublicKey } from 'nostr-tools/pure' import { channelMessageEvent } from 'nostr-tools/nip28' import { hexToBytes } from 'nostr-tools/utils' import WebSocket from 'ws' // ── Config ──────────────────────────────────────────────────────────────────── const RELAY_URL = process.env.RELAY_URL || 'ws://localhost:4869' const GUEST_PRIVKEY_HEX = process.env.GUEST_PRIVKEY const CHANNEL_EVENT_ID = process.env.CHANNEL_EVENT_ID if (!GUEST_PRIVKEY_HEX || !CHANNEL_EVENT_ID) { console.error('Usage: GUEST_PRIVKEY= CHANNEL_EVENT_ID= node test-sprout-proxy.mjs') process.exit(1) } const secretKey = hexToBytes(GUEST_PRIVKEY_HEX) const pubkey = getPublicKey(secretKey) // ── Helpers ─────────────────────────────────────────────────────────────────── const results = [] function pass(name, detail) { results.push({ name, ok: true }) console.log(` ✅ ${name}${detail ? ` — ${detail}` : ''}`) } function fail(name, detail) { results.push({ name, ok: false }) console.log(` ❌ ${name}${detail ? ` — ${detail}` : ''}`) } /** Collect events from a subscription until EOSE, with timeout. */ function list(relay, filters, timeoutMs = 8000) { return new Promise((resolve, reject) => { const events = [] const timer = setTimeout(() => { sub.close() resolve(events) }, timeoutMs) const sub = relay.subscribe(filters, { onevent(evt) { events.push(evt) }, oneose() { clearTimeout(timer) sub.close() resolve(events) }, }) }) } // ── Main ────────────────────────────────────────────────────────────────────── async function main() { console.log('═'.repeat(64)) console.log(' nostr-tools v2.23 E2E TEST against sprout-proxy') console.log('═'.repeat(64)) console.log(` Relay: ${RELAY_URL}`) console.log(` Pubkey: ${pubkey.slice(0, 24)}...`) console.log() // ── 1. Connect + NIP-42 Auth ────────────────────────────────────────── console.log('[1/6] Connect + NIP-42 Authentication') // nostr-tools' Relay class calls onauth when it receives an AUTH challenge. // We set it before connecting so the handshake is automatic. const relay = new Relay(RELAY_URL, { websocketImplementation: WebSocket }) // Wire up the NIP-42 auth callback BEFORE connecting. // nostr-tools calls: relay.auth(onauth) → onauth(makeAuthEvent(url, challenge)) // So onauth receives an unsigned event template and must return a signed event. let authResolved = false relay.onauth = async (authEventTemplate) => { const signed = finalizeEvent(authEventTemplate, secretKey) authResolved = true return signed } try { await relay.connect() } catch (e) { fail('Connect', e.message) process.exit(1) } // Wait a moment for the AUTH challenge to arrive and be handled // nostr-tools' onauth is called synchronously when the AUTH message arrives await new Promise(r => setTimeout(r, 1500)) if (authResolved) { pass('NIP-42 Auth', `challenge received, signed as ${pubkey.slice(0, 16)}...`) } else { // Auth might still work reactively — the proxy sends CLOSED/OK auth-required // which triggers nostr-tools to call relay.auth() console.log(' ⏳ No proactive AUTH challenge yet — testing reactive auth...') } // ── 2. Channel Discovery (kind:40) ──────────────────────────────────── console.log('\n[2/6] Channel Discovery (kind:40)') try { const channels = await list(relay, [{ kinds: [40], limit: 100 }]) if (channels.length > 0) { for (const evt of channels) { try { const c = JSON.parse(evt.content) console.log(` • ${c.name || '(unnamed)'} (id: ${evt.id.slice(0, 24)}...)`) } catch { console.log(` • (id: ${evt.id.slice(0, 24)}...)`) } } pass('Channel Discovery', `${channels.length} channel(s)`) } else { fail('Channel Discovery', 'no kind:40 events returned') } } catch (e) { fail('Channel Discovery', e.message) } // ── 3. Channel Metadata (kind:41) ───────────────────────────────────── console.log('\n[3/6] Channel Metadata (kind:41)') try { const meta = await list(relay, [{ kinds: [41], '#e': [CHANNEL_EVENT_ID], limit: 5 }]) if (meta.length > 0) { const c = JSON.parse(meta[0].content) pass('Channel Metadata', `name="${c.name}", about="${(c.about || '').slice(0, 50)}"`) } else { fail('Channel Metadata', 'no kind:41 events') } } catch (e) { fail('Channel Metadata', e.message) } // ── 4. Send Channel Message (kind:42 via NIP-28) ────────────────────── console.log('\n[4/6] Send Channel Message (NIP-28 channelMessageEvent)') const ts = Math.floor(Date.now() / 1000) const msgText = `Hello from nostr-tools! 📦 [${ts}]` let sentEvent try { sentEvent = channelMessageEvent({ channel_create_event_id: CHANNEL_EVENT_ID, relay_url: RELAY_URL, content: msgText, created_at: ts, }, secretKey) await relay.publish(sentEvent) pass('Send Message', `"${msgText}" (id: ${sentEvent.id.slice(0, 16)}...)`) } catch (e) { fail('Send Message', e.message) } // ── 5. Read Messages (kind:42) ──────────────────────────────────────── console.log('\n[5/6] Read Messages (kind:42 query)') try { const msgs = await list(relay, [{ kinds: [42], '#e': [CHANNEL_EVENT_ID], limit: 10 }]) let foundOurs = false console.log(` ${msgs.length} message(s):`) for (const evt of msgs) { const marker = evt.content.includes('nostr-tools') ? '→ ' : ' ' console.log(` ${marker}[${evt.pubkey.slice(0, 12)}...] ${evt.content.slice(0, 65)}`) if (evt.content.includes('nostr-tools')) foundOurs = true } if (foundOurs) { pass('Read Messages', `found our message among ${msgs.length}`) } else if (msgs.length > 0) { pass('Read Messages', `${msgs.length} messages (ours may be propagating)`) } else { fail('Read Messages', 'no messages') } } catch (e) { fail('Read Messages', e.message) } // ── 6. Live Streaming ───────────────────────────────────────────────── console.log('\n[6/6] Live Streaming (real-time event delivery)') try { const streamResult = await new Promise((resolve) => { const timer = setTimeout(() => { sub.close(); resolve(false) }, 8000) const sub = relay.subscribe([{ kinds: [42], '#e': [CHANNEL_EVENT_ID], since: ts, }], { onevent(evt) { if (evt.content.includes('stream-ping')) { clearTimeout(timer) sub.close() resolve(true) } }, oneose() { // After EOSE, publish a new message that should arrive live const ping = channelMessageEvent({ channel_create_event_id: CHANNEL_EVENT_ID, relay_url: RELAY_URL, content: `nostr-tools stream-ping ${Date.now()}`, created_at: Math.floor(Date.now() / 1000), }, secretKey) relay.publish(ping).catch(() => {}) }, }) }) if (streamResult) { pass('Live Streaming', 'received real-time event') } else { fail('Live Streaming', 'timed out') } } catch (e) { fail('Live Streaming', e.message) } // ── Summary ─────────────────────────────────────────────────────────── relay.close() console.log('\n' + '═'.repeat(64)) console.log(' RESULTS') console.log('═'.repeat(64)) let allPass = true for (const r of results) { console.log(` ${r.ok ? '✅ PASS' : '❌ FAIL'} ${r.name}`) if (!r.ok) allPass = false } console.log('═'.repeat(64)) console.log(allPass ? ' 🎉 ALL TESTS PASSED — nostr-tools works with sprout-proxy!' : ' ⚠️ Some tests failed') console.log('═'.repeat(64)) process.exit(allPass ? 0 : 1) } main().catch(e => { console.error('Fatal:', e); process.exit(1) })