Files
buzz/scripts/test-proxy-nostr-tools.mjs

261 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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) })