mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
261 lines
9.4 KiB
JavaScript
261 lines
9.4 KiB
JavaScript
/**
|
|||
|
|
* 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=<hex> CHANNEL_EVENT_ID=<hex> 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) })
|