Remove unused NIP-28 proxy

Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr
2026-06-27 19:39:24 -04:00
parent 982fa1f483
commit 9edfd33fac
38 changed files with 89 additions and 10425 deletions

View File

@@ -1,740 +0,0 @@
#!/usr/bin/env bash
# =============================================================================
# e2e-relay-membership.sh — End-to-end test for NIP-43 relay membership
# =============================================================================
# Tests relay-level membership enforcement and admin commands.
#
# Prerequisites:
# - Docker services running (postgres, redis, minio)
# - Relay built: cargo build --release --bin buzz-relay
# - nak available on PATH (for event signing)
#
# What it tests:
# 1. Non-member REST calls are rejected (401/403)
# 2. Owner adds admin via kind:9030
# 3. Admin adds member via kind:9030
# 4. Owner changes member role via kind:9032
# 5. Admin removes member via kind:9031
# 6. Members cannot add others (permission denied)
# 7. Admins cannot change roles (permission denied)
# 8. Owner cannot be removed
# 9. GET /api/relay/members returns correct list
# 10. GET /api/relay/members/me returns correct role
# 11. NIP-11 self field is advertised
# 12. kind:13534 membership list published after add
# 13. kind:8000 member-added announcement published
# 14. kind:28936 leave request (happy path)
# 15. kind:8001 member-removed announcement after leave
# 16. Leave request without NIP-70 - tag is rejected
# 17. Stale admin command rejected (replay protection)
# 18. Owner cannot leave (lockout prevention)
# 19. Admin cannot add another admin
# 20. Admin cannot remove the owner
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${REPO_ROOT}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() { echo -e "${BLUE}[e2e-membership]${NC} $*"; }
success() { echo -e "${GREEN}[e2e-membership]${NC} ✓ $*"; }
warn() { echo -e "${YELLOW}[e2e-membership]${NC} ⚠ $*"; }
# Failure counter — tests record failures and continue; fatal() exits immediately.
FAILURES=0
fail() {
echo -e "${RED}[e2e-membership]${NC} ✗ $*" >&2
FAILURES=$((FAILURES + 1))
}
fatal() {
echo -e "${RED}[e2e-membership]${NC} ✗ FATAL: $*" >&2
cleanup
exit 1
}
# ── Cleanup ───────────────────────────────────────────────────────────────────
RELAY_PID=""
cleanup() {
if [[ -n "$RELAY_PID" ]]; then
kill "$RELAY_PID" 2>/dev/null || true
wait "$RELAY_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT
# ── Helpers: keypair generation ───────────────────────────────────────────────
generate_keypair() {
openssl rand -hex 32
}
derive_pubkey() {
local privkey="$1"
python3 -c "
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
def point_add(p1, p2):
if p1 is None: return p2
if p2 is None: return p1
x1, y1 = p1; x2, y2 = p2
if x1 == x2 and y1 != y2: return None
if x1 == x2: lam = (3*x1*x1) * pow(2*y1, P-2, P) % P
else: lam = (y2-y1) * pow(x2-x1, P-2, P) % P
x3 = (lam*lam - x1 - x2) % P
y3 = (lam*(x1-x3) - y1) % P
return (x3, y3)
def scalar_mult(k, point):
result = None; addend = point
while k:
if k & 1: result = point_add(result, addend)
addend = point_add(addend, addend)
k >>= 1
return result
pub = scalar_mult(int('$privkey', 16), (Gx, Gy))
print(format(pub[0], '064x'))
"
}
# ── Helper: sign and send a Nostr event via nak ───────────────────────────────
#
# Usage: send_event <privkey> <kind> <content> [tag ...]
# Each tag is passed as a separate argument in nak --tag format: "key=value"
#
# Returns the relay response on stdout.
send_event() {
local privkey="$1"
local kind="$2"
local content="$3"
shift 3
local tag_args=()
for t in "$@"; do
tag_args+=(--tag "$t")
done
nak event \
--sec "$privkey" \
--kind "$kind" \
--content "$content" \
"${tag_args[@]}" \
ws://localhost:3000 2>&1
}
# ── Helper: REST call with X-Pubkey header (dev mode) ────────────────────────
#
# When BUZZ_REQUIRE_AUTH_TOKEN=false the relay accepts an X-Pubkey header
# containing the caller's hex pubkey — no token minting required.
# This is the correct pattern for dev-mode E2E tests.
#
# Returns the HTTP status code on stdout.
rest_call() {
local privkey="$1"
local method="$2"
local path="$3"
local body="${4:-}"
local pubkey
pubkey=$(derive_pubkey "$privkey")
local curl_args=(-s -o /dev/null -w "%{http_code}" -X "$method"
-H "X-Pubkey: $pubkey")
if [[ -n "$body" ]]; then
curl_args+=(-H "Content-Type: application/json" -d "$body")
fi
curl "${curl_args[@]}" "http://localhost:3000${path}"
}
# ── Helper: REST GET, return body ─────────────────────────────────────────────
rest_get_body() {
local privkey="$1"
local path="$2"
local pubkey
pubkey=$(derive_pubkey "$privkey")
curl -s -X GET \
-H "X-Pubkey: $pubkey" \
"http://localhost:3000${path}"
}
# ── Helper: check relay OK response ──────────────────────────────────────────
#
# Returns 0 if the response is ["OK", <id>, true, ...], 1 otherwise.
is_ok() {
echo "$1" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
if isinstance(data, list) and len(data) >= 3 and data[0] == 'OK' and data[2] is True:
sys.exit(0)
except: pass
sys.exit(1)
" 2>/dev/null
}
# ── Helper: check relay rejection (OK false) ─────────────────────────────────
#
# Returns 0 if the response is ["OK", <id>, false, ...], 1 otherwise.
is_rejected() {
echo "$1" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
if isinstance(data, list) and len(data) >= 3 and data[0] == 'OK' and data[2] is False:
sys.exit(0)
except: pass
sys.exit(1)
" 2>/dev/null
}
# ── Start relay ───────────────────────────────────────────────────────────────
log "Starting relay with relay membership enforcement enabled..."
if [[ -f .env ]]; then
set -o allexport
source .env
set +o allexport
fi
export BUZZ_BIND_ADDR="0.0.0.0:3000"
export RELAY_URL="ws://localhost:3000"
export RUST_LOG="buzz_relay=warn"
export BUZZ_REQUIRE_AUTH_TOKEN=false
export BUZZ_REQUIRE_RELAY_MEMBERSHIP=true
# Generate owner keypair BEFORE relay start — main.rs requires RELAY_OWNER_PUBKEY
log "Generating owner keypair..."
OWNER_PRIVKEY=$(generate_keypair)
OWNER_PUBKEY=$(derive_pubkey "$OWNER_PRIVKEY")
export RELAY_OWNER_PUBKEY="$OWNER_PUBKEY"
log "Owner pubkey: $OWNER_PUBKEY"
# Generate a stable relay signing key for NIP-43 self-signed events (kind:13534, etc.)
RELAY_SK=$(generate_keypair)
export BUZZ_RELAY_PRIVATE_KEY="$RELAY_SK"
log "Relay signing key set (NIP-43 self-signed events enabled)"
# Kill any existing relay
pkill -f "buzz-relay" 2>/dev/null || true
sleep 1
./target/release/buzz-relay > /tmp/buzz-relay-membership-e2e.log 2>&1 &
RELAY_PID=$!
for i in $(seq 1 15); do
if curl -s http://localhost:3000/ -H "Accept: application/nostr+json" | grep -q "Buzz"; then
break
fi
if [[ $i -eq 15 ]]; then
fatal "Relay did not start. Check /tmp/buzz-relay-membership-e2e.log"
fi
sleep 1
done
success "Relay started (PID $RELAY_PID)"
# ── Generate identities ───────────────────────────────────────────────────────
# Generate remaining keypairs (after relay is running)
log "Generating remaining keypairs..."
ADMIN_PRIVKEY=$(generate_keypair)
ADMIN_PUBKEY=$(derive_pubkey "$ADMIN_PRIVKEY")
MEMBER_PRIVKEY=$(generate_keypair)
MEMBER_PUBKEY=$(derive_pubkey "$MEMBER_PRIVKEY")
NONMEMBER_PRIVKEY=$(generate_keypair)
NONMEMBER_PUBKEY=$(derive_pubkey "$NONMEMBER_PRIVKEY")
ATTACKER_PRIVKEY=$(generate_keypair)
ATTACKER_PUBKEY=$(derive_pubkey "$ATTACKER_PRIVKEY")
log " Owner: ${OWNER_PUBKEY:0:16}..."
log " Admin: ${ADMIN_PUBKEY:0:16}..."
log " Member: ${MEMBER_PUBKEY:0:16}..."
log " Non-member:${NONMEMBER_PUBKEY:0:16}..."
log " Attacker: ${ATTACKER_PUBKEY:0:16}..."
# ── Test 1: Non-member REST calls are rejected ────────────────────────────────
log "Test 1: Non-member REST calls should be rejected..."
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/channels)
if [[ "$STATUS" == "401" || "$STATUS" == "403" ]]; then
success "Unauthenticated request rejected ($STATUS)"
else
fail "Expected 401/403 for unauthenticated request, got $STATUS"
fi
# Non-member with valid auth should also be rejected
STATUS=$(rest_call "$NONMEMBER_PRIVKEY" GET /api/channels)
if [[ "$STATUS" == "401" || "$STATUS" == "403" ]]; then
success "Non-member request rejected ($STATUS)"
else
fail "Non-member got $STATUS — expected 401 or 403 (membership enforcement bypass!)"
fi
# ── Test 1b: Non-member WebSocket event should be rejected ───────────────────
log "Test 1b: Non-member WebSocket event should be rejected..."
# Non-member tries to send a stream message over WebSocket.
# The relay enforces NIP-43 during NIP-42 AUTH, so the event will be rejected
# before it is processed. Channel ID is irrelevant — auth fires first.
RESULT=$(send_event "$NONMEMBER_PRIVKEY" 9 "h=00000000-0000-0000-0000-000000000000" 2>&1 || true)
if echo "$RESULT" | grep -qi 'not a relay member\|restricted\|auth\|false'; then
success "Non-member WebSocket event rejected"
else
warn "Could not verify WebSocket non-member rejection: $RESULT"
fi
# ── Test 2: Owner adds admin (kind:9030) ──────────────────────────────────────
log "Test 2: Owner adds admin via kind:9030..."
RESULT=$(send_event "$OWNER_PRIVKEY" 9030 "" \
"p=$ADMIN_PUBKEY" \
"role=admin")
if is_ok "$RESULT"; then
success "Owner added admin (kind:9030 accepted)"
else
fail "Owner failed to add admin: $RESULT"
fi
sleep 1
# Verify admin appears in member list
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
if echo "$MEMBERS" | grep -q "$ADMIN_PUBKEY"; then
success "Admin pubkey appears in /api/relay/members"
else
fail "Admin pubkey not found in /api/relay/members: $MEMBERS"
fi
# ── Test 3: Admin adds member (kind:9030) ─────────────────────────────────────
log "Test 3: Admin adds member via kind:9030..."
RESULT=$(send_event "$ADMIN_PRIVKEY" 9030 "" \
"p=$MEMBER_PUBKEY" \
"role=member")
if is_ok "$RESULT"; then
success "Admin added member (kind:9030 accepted)"
else
fail "Admin failed to add member: $RESULT"
fi
sleep 1
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
if echo "$MEMBERS" | grep -q "$MEMBER_PUBKEY"; then
success "Member pubkey appears in /api/relay/members"
else
fail "Member pubkey not found in /api/relay/members: $MEMBERS"
fi
# ── Test 4: Owner changes role (kind:9032) ────────────────────────────────────
log "Test 4: Owner changes member role to admin via kind:9032..."
RESULT=$(send_event "$OWNER_PRIVKEY" 9032 "" \
"p=$MEMBER_PUBKEY" \
"role=admin")
if is_ok "$RESULT"; then
success "Owner changed member role (kind:9032 accepted)"
else
fail "Owner failed to change role: $RESULT"
fi
sleep 1
# Verify role changed
MEMBER_INFO=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
if echo "$MEMBER_INFO" | python3 -c "
import sys, json
data = json.load(sys.stdin)
members = data if isinstance(data, list) else data.get('members', [])
for m in members:
if m.get('pubkey', '').startswith('${MEMBER_PUBKEY:0:8}') or m.get('pubkey') == '${MEMBER_PUBKEY}':
if m.get('role') == 'admin':
sys.exit(0)
sys.exit(1)
" 2>/dev/null; then
success "Member role updated to admin in /api/relay/members"
else
fail "Role change not reflected in /api/relay/members response"
fi
# Reset member back to 'member' role for subsequent tests
send_event "$OWNER_PRIVKEY" 9032 "" \
"p=$MEMBER_PUBKEY" \
"role=member" > /dev/null 2>&1 || true
sleep 1
# ── Test 5: Admin removes member (kind:9031) ──────────────────────────────────
log "Test 5: Admin removes member via kind:9031..."
RESULT=$(send_event "$ADMIN_PRIVKEY" 9031 "" \
"p=$MEMBER_PUBKEY")
if is_ok "$RESULT"; then
success "Admin removed member (kind:9031 accepted)"
else
fail "Admin failed to remove member: $RESULT"
fi
sleep 1
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
if echo "$MEMBERS" | grep -q "$MEMBER_PUBKEY"; then
fail "Member pubkey still present in /api/relay/members after removal"
else
success "Member pubkey absent from /api/relay/members after removal"
fi
# Re-add member for permission tests
send_event "$ADMIN_PRIVKEY" 9030 "" \
"p=$MEMBER_PUBKEY" \
"role=member" > /dev/null 2>&1 || true
sleep 1
# ── Test 6: Members cannot add others ────────────────────────────────────────
log "Test 6: Member attempts to add non-member (should be denied)..."
RESULT=$(send_event "$MEMBER_PRIVKEY" 9030 "" \
"p=$NONMEMBER_PUBKEY" \
"role=member")
if is_ok "$RESULT"; then
fail "Member was able to add another user (should be denied)"
elif is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'restricted\|denied\|forbidden\|unauthorized\|permission'; then
success "Member add attempt rejected"
else
fail "Ambiguous response for member add attempt: $RESULT"
fi
# ── Test 7: Admins cannot change roles ───────────────────────────────────────
log "Test 7: Admin attempts to change owner role (should be denied)..."
RESULT=$(send_event "$ADMIN_PRIVKEY" 9032 "" \
"p=$OWNER_PUBKEY" \
"role=member")
if is_ok "$RESULT"; then
fail "Admin was able to change owner role (should be denied)"
elif is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'restricted\|denied\|forbidden\|unauthorized\|permission'; then
success "Admin role-change on owner rejected"
else
fail "Ambiguous response for admin role-change attempt: $RESULT"
fi
# ── Test 8: Owner cannot be removed ──────────────────────────────────────────
log "Test 8: Admin attempts to remove owner (should be denied)..."
RESULT=$(send_event "$ADMIN_PRIVKEY" 9031 "" \
"p=$OWNER_PUBKEY")
if is_ok "$RESULT"; then
fail "Admin was able to remove the owner (should be denied)"
elif is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'restricted\|denied\|forbidden\|unauthorized\|permission'; then
success "Owner removal attempt rejected"
else
fail "Ambiguous response for owner removal attempt: $RESULT"
fi
# Verify owner still present
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
if echo "$MEMBERS" | grep -q "$OWNER_PUBKEY"; then
success "Owner still present in /api/relay/members"
else
fail "Owner missing from /api/relay/members after removal attempt"
fi
# ── Test 9: GET /api/relay/members returns correct list ───────────────────────
log "Test 9: GET /api/relay/members returns expected members..."
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
STATUS=$(rest_call "$OWNER_PRIVKEY" GET /api/relay/members)
if [[ "$STATUS" == "200" ]]; then
success "GET /api/relay/members returned 200"
else
fail "GET /api/relay/members returned $STATUS (expected 200)"
fi
for pubkey in "$OWNER_PUBKEY" "$ADMIN_PUBKEY" "$MEMBER_PUBKEY"; do
if echo "$MEMBERS" | grep -q "$pubkey"; then
success " ${pubkey:0:16}... present in member list"
else
fail " ${pubkey:0:16}... missing from member list"
fi
done
if echo "$MEMBERS" | grep -q "$NONMEMBER_PUBKEY"; then
fail "Non-member ${NONMEMBER_PUBKEY:0:16}... should not appear in member list"
else
success "Non-member correctly absent from member list"
fi
# ── Test 10: GET /api/relay/members/me returns correct role ──────────────────
log "Test 10: GET /api/relay/members/me returns correct role..."
for pair in "${OWNER_PRIVKEY}:owner" "${ADMIN_PRIVKEY}:admin" "${MEMBER_PRIVKEY}:member"; do
privkey="${pair%%:*}"
expected_role="${pair##*:}"
STATUS=$(rest_call "$privkey" GET /api/relay/members/me)
BODY=$(rest_get_body "$privkey" /api/relay/members/me)
if [[ "$STATUS" == "200" ]]; then
if echo "$BODY" | grep -q "\"$expected_role\""; then
success " /api/relay/members/me: $expected_role role confirmed (status $STATUS)"
else
warn " /api/relay/members/me: expected role '$expected_role', got: $BODY"
fi
else
warn " /api/relay/members/me returned $STATUS for $expected_role"
fi
done
# Non-member should get 403/404
STATUS=$(rest_call "$NONMEMBER_PRIVKEY" GET /api/relay/members/me)
if [[ "$STATUS" == "403" || "$STATUS" == "404" ]]; then
success "Non-member /api/relay/members/me correctly rejected ($STATUS)"
else
warn "Non-member /api/relay/members/me returned $STATUS (expected 403/404)"
fi
# ── Test 11: NIP-11 self field is advertised ──────────────────────────────────
log "Test 11: NIP-11 self field is advertised..."
RELAY_HTTP="http://localhost:3000"
SELF_PK=$(curl -s -H "Accept: application/nostr+json" "$RELAY_HTTP" | jq -r '.self // empty')
if [[ -n "$SELF_PK" && "$SELF_PK" != "null" ]]; then
success "NIP-11 self field present: ${SELF_PK:0:16}..."
else
fail "NIP-11 self field missing from relay info document"
fi
# ── Test 12: kind:13534 membership list published after add ───────────────────
#
# After Test 3 added a member, the relay should have published a signed
# kind:13534 membership list event to the DB.
log "Test 12: kind:13534 membership list published after member add..."
COUNT=$(psql "$DATABASE_URL" -t -c "SELECT count(*) FROM events WHERE kind=13534;" 2>/dev/null | tr -d ' \n')
if [[ -n "$COUNT" && "$COUNT" -gt 0 ]]; then
success "kind:13534 membership list found in DB ($COUNT row(s))"
else
fail "kind:13534 not found in DB — relay may not be publishing membership lists"
fi
# ── Test 13: kind:8000 member-added announcement published ────────────────────
log "Test 13: kind:8000 member-added announcement published..."
COUNT=$(psql "$DATABASE_URL" -t -c "SELECT count(*) FROM events WHERE kind=8000;" 2>/dev/null | tr -d ' \n')
if [[ -n "$COUNT" && "$COUNT" -gt 0 ]]; then
success "kind:8000 member-added announcement found in DB ($COUNT row(s))"
else
fail "kind:8000 not found in DB — relay may not be publishing member-added announcements"
fi
# ── Test 14: kind:28936 leave request (happy path) ────────────────────────────
#
# Member sends a valid leave request with the NIP-70 protected-event `-` tag.
# The relay should accept it and remove the member.
log "Test 14: kind:28936 leave request (happy path)..."
RESULT=$(send_event "$MEMBER_PRIVKEY" 28936 "" "-=")
if is_ok "$RESULT"; then
success "Leave request accepted (kind:28936 with NIP-70 - tag)"
else
fail "Leave request rejected: $RESULT"
fi
sleep 1
# Verify member was removed
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
if echo "$MEMBERS" | grep -q "$MEMBER_PUBKEY"; then
fail "Member still present in /api/relay/members after leave request"
else
success "Member correctly absent from /api/relay/members after leave"
fi
# ── Test 15: kind:8001 member-removed announcement after leave ────────────────
log "Test 15: kind:8001 member-removed announcement published after leave..."
COUNT=$(psql "$DATABASE_URL" -t -c "SELECT count(*) FROM events WHERE kind=8001;" 2>/dev/null | tr -d ' \n')
if [[ -n "$COUNT" && "$COUNT" -gt 0 ]]; then
success "kind:8001 member-removed announcement found in DB ($COUNT row(s))"
else
fail "kind:8001 not found in DB — relay may not be publishing member-removed announcements"
fi
# ── Test 16: Leave request without NIP-70 - tag is rejected ──────────────────
#
# Re-add the member first, then send a leave WITHOUT the required `-` tag.
# The relay must reject it (NIP-70 enforcement).
log "Test 16: Leave request without NIP-70 - tag is rejected..."
# Re-add member
send_event "$ADMIN_PRIVKEY" 9030 "" \
"p=$MEMBER_PUBKEY" \
"role=member" > /dev/null 2>&1 || true
sleep 1
# Send leave without the - tag
RESULT=$(send_event "$MEMBER_PRIVKEY" 28936 "")
if is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'restricted\|denied\|forbidden\|nip-70\|protected\|false'; then
success "Leave without NIP-70 - tag correctly rejected"
else
fail "Leave without NIP-70 - tag was accepted (should be rejected): $RESULT"
fi
# ── Test 17: Stale admin command rejected (replay protection) ─────────────────
#
# An admin command with a created_at more than 5 minutes in the past must be
# rejected to prevent replay attacks.
log "Test 17: Stale admin command rejected (replay protection)..."
OLD_TS=$(($(date +%s) - 300))
# Use nak directly to control the timestamp; pipe output to the relay
RESULT=$(nak event \
--sec "$ADMIN_PRIVKEY" \
--kind 9030 \
--created-at "$OLD_TS" \
--tag "p=$ATTACKER_PUBKEY" \
--tag "role=member" \
--content "" \
ws://localhost:3000 2>&1 || true)
if is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'stale\|old\|expired\|restricted\|false'; then
success "Stale admin command (created_at -5m) rejected"
else
fail "Stale admin command was accepted (replay protection missing): $RESULT"
fi
# Verify attacker was NOT added
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
if echo "$MEMBERS" | grep -q "$ATTACKER_PUBKEY"; then
fail "Attacker was added via stale command (replay protection bypass!)"
else
success "Attacker not present in member list after stale command"
fi
# ── Test 18: Owner cannot leave (lockout prevention) ─────────────────────────
#
# The relay must refuse a leave request from the owner to prevent lockout.
log "Test 18: Owner cannot leave (lockout prevention)..."
RESULT=$(send_event "$OWNER_PRIVKEY" 28936 "" "-=")
if is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'owner\|restricted\|denied\|forbidden\|false'; then
success "Owner leave request correctly rejected"
else
fail "Owner leave request was accepted (lockout prevention missing): $RESULT"
fi
# Verify owner still present
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
if echo "$MEMBERS" | grep -q "$OWNER_PUBKEY"; then
success "Owner still present in /api/relay/members after leave attempt"
else
fail "Owner missing from /api/relay/members after leave attempt"
fi
# ── Test 19: Admin cannot add another admin ───────────────────────────────────
#
# Admins can only add members. Elevating to admin requires owner privilege.
log "Test 19: Admin cannot add another admin..."
RESULT=$(send_event "$ADMIN_PRIVKEY" 9030 "" \
"p=$ATTACKER_PUBKEY" \
"role=admin")
if is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'restricted\|denied\|forbidden\|unauthorized\|permission\|false'; then
success "Admin cannot add another admin (correctly rejected)"
else
fail "Admin was able to add an admin (privilege escalation!): $RESULT"
fi
# Verify attacker was NOT added as admin
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
if echo "$MEMBERS" | python3 -c "
import sys, json
data = json.load(sys.stdin)
members = data if isinstance(data, list) else data.get('members', [])
for m in members:
if m.get('pubkey') == '${ATTACKER_PUBKEY}' and m.get('role') == 'admin':
sys.exit(0)
sys.exit(1)
" 2>/dev/null; then
fail "Attacker has admin role in member list (privilege escalation confirmed!)"
else
success "Attacker not present as admin in member list"
fi
# ── Test 20: Admin cannot remove the owner ────────────────────────────────────
log "Test 20: Admin cannot remove the owner..."
RESULT=$(send_event "$ADMIN_PRIVKEY" 9031 "" \
"p=$OWNER_PUBKEY")
if is_rejected "$RESULT" || echo "$RESULT" | grep -qi 'restricted\|denied\|forbidden\|unauthorized\|permission\|false'; then
success "Admin cannot remove the owner (correctly rejected)"
else
fail "Admin was able to remove the owner (privilege escalation!): $RESULT"
fi
# Verify owner still present
MEMBERS=$(rest_get_body "$OWNER_PRIVKEY" /api/relay/members)
if echo "$MEMBERS" | grep -q "$OWNER_PUBKEY"; then
success "Owner still present in /api/relay/members after admin removal attempt"
else
fail "Owner missing from /api/relay/members — admin removed owner!"
fi
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
echo "Final member list:"
echo "─────────────────────"
rest_get_body "$OWNER_PRIVKEY" /api/relay/members | python3 -m json.tool 2>/dev/null || true
echo ""
if [[ "$FAILURES" -eq 0 ]]; then
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} ✅ All NIP-43 relay membership E2E tests passed!${NC}"
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
else
echo -e "${RED}════════════════════════════════════════════════════════${NC}"
echo -e "${RED} ❌ ${FAILURES} test(s) failed!${NC}"
echo -e "${RED}════════════════════════════════════════════════════════${NC}"
exit 1
fi

View File

@@ -1,140 +0,0 @@
#!/usr/bin/env bash
# Live e2e test for buzz-proxy — requires relay on :3000 and proxy on :4869
set -euo pipefail
PROXY_URL="ws://0.0.0.0:4869"
PROXY_HTTP="http://localhost:4869"
ADMIN_SECRET="test-admin-secret"
PASS=0
FAIL=0
ok() { PASS=$((PASS+1)); echo " ✅ $1"; }
fail() { FAIL=$((FAIL+1)); echo " ❌ $1"; }
echo "═══ buzz-proxy e2e tests ═══"
echo ""
# ── Test 1: NIP-11 ──────────────────────────────────────────────────────
echo "1. NIP-11 relay info"
NIP11=$(curl -sf "$PROXY_HTTP" -H "Accept: application/nostr+json")
if echo "$NIP11" | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['name']=='buzz-proxy'" 2>/dev/null; then
ok "NIP-11 returns valid relay info"
else
fail "NIP-11 check failed"
fi
# ── Test 2: Register guest ──────────────────────────────────────────────
echo "2. Guest registration (pubkey-based)"
GUEST_SK=$(nak key generate)
GUEST_PK=$(nak key public "$GUEST_SK")
CHANNEL_UUID="6f953bb4-f761-4bc2-98de-392470c2b897"
REG=$(curl -sf -X POST "$PROXY_HTTP/admin/guests" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d "{\"pubkey\": \"$GUEST_PK\", \"channels\": \"$CHANNEL_UUID\"}")
if echo "$REG" | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['pubkey']=='$GUEST_PK'" 2>/dev/null; then
ok "Guest registered: $GUEST_PK"
else
fail "Guest registration failed: $REG"
fi
# ── Test 3: List guests ─────────────────────────────────────────────────
echo "3. List guests"
LIST=$(curl -sf "$PROXY_HTTP/admin/guests" -H "Authorization: Bearer $ADMIN_SECRET")
if echo "$LIST" | python3 -c "import sys,json; d=json.load(sys.stdin); assert len(d['guests'])>=1" 2>/dev/null; then
ok "Guest list returns registered guests"
else
fail "Guest list failed: $LIST"
fi
# ── Test 4: Admin auth required ─────────────────────────────────────────
echo "4. Admin auth enforcement"
NOAUTH=$(curl -s -o /dev/null -w "%{http_code}" "$PROXY_HTTP/admin/guests")
if [ "$NOAUTH" = "401" ]; then
ok "Admin endpoint rejects unauthenticated requests"
else
fail "Expected 401, got $NOAUTH"
fi
# ── Test 5: WebSocket AUTH challenge ────────────────────────────────────
echo "5. WebSocket NIP-42 challenge"
CHALLENGE=$(echo "" | timeout 3 websocat -t -1 "$PROXY_URL" 2>/dev/null | head -1)
if echo "$CHALLENGE" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); assert d[0]=='AUTH'" 2>/dev/null; then
ok "Proxy sends NIP-42 AUTH challenge on connect"
else
fail "No AUTH challenge received: $CHALLENGE"
fi
# ── Test 6: Full NIP-42 handshake (pubkey-based, no token) ─────────────
echo "6. Full NIP-42 auth handshake (pubkey-based)"
# Use a Python script to do the full handshake on a single WebSocket connection.
RESPONSE=$(uv run python3 -c "
import asyncio, json, subprocess, sys
async def test():
import websockets
async with websockets.connect('$PROXY_URL') as ws:
# 1. Receive AUTH challenge
msg = json.loads(await ws.recv())
assert msg[0] == 'AUTH', f'Expected AUTH, got {msg[0]}'
challenge = msg[1]
# 2. Create signed auth event with nak
auth_json = subprocess.check_output([
'nak', 'event', '--sec', '$GUEST_SK', '-k', '22242',
'--tag', 'relay=$PROXY_URL',
'--tag', f'challenge={challenge}',
'-c', ''
], text=True).strip()
# 3. Send AUTH response
await ws.send(json.dumps(['AUTH', json.loads(auth_json)]))
# 4. Read OK
ok_msg = json.loads(await ws.recv())
print(json.dumps(ok_msg))
asyncio.run(test())
" 2>&1) || true
echo " Response: $RESPONSE"
if echo "$RESPONSE" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); assert d[0]=='OK' and d[2]==True" 2>/dev/null; then
ok "NIP-42 auth succeeded (pubkey-based, no token)"
else
# Fall back: try without websockets library (use websocat with named pipe)
fail "NIP-42 auth handshake: $RESPONSE"
fi
# ── Test 7: Create invite token ─────────────────────────────────────────
echo "7. Invite token creation"
INVITE=$(curl -sf -X POST "$PROXY_HTTP/admin/invite" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d "{\"channels\": \"$CHANNEL_UUID\", \"hours\": 1, \"max_uses\": 5}")
TOKEN=$(echo "$INVITE" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])" 2>/dev/null)
if [ -n "$TOKEN" ]; then
ok "Invite token created: ${TOKEN:0:30}..."
else
fail "Invite token creation failed: $INVITE"
fi
# ── Test 8: Revoke guest ────────────────────────────────────────────────
echo "8. Guest revocation"
REVOKE=$(curl -sf -X DELETE "$PROXY_HTTP/admin/guests" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d "{\"pubkey\": \"$GUEST_PK\"}")
if echo "$REVOKE" | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['revoked']==True" 2>/dev/null; then
ok "Guest revoked"
else
fail "Guest revocation failed: $REVOKE"
fi
# ── Summary ─────────────────────────────────────────────────────────────
echo ""
echo "═══ Results: $PASS passed, $FAIL failed ═══"
[ "$FAIL" -eq 0 ] && exit 0 || exit 1

View File

@@ -1,89 +0,0 @@
#!/usr/bin/env bash
# End-to-end test for buzz-proxy
# Prerequisites:
# - Buzz relay running on :3000 (just relay)
# - Buzz proxy running on :4869 (just proxy)
# - websocat installed (cargo install websocat)
# - curl installed
# - jq installed
set -euo pipefail
PROXY_URL="${PROXY_URL:-ws://localhost:4869}"
PROXY_HTTP="${PROXY_HTTP:-http://localhost:4869}"
RELAY_HTTP="${RELAY_HTTP:-http://localhost:3000}"
echo "=== Buzz Proxy E2E Test ==="
echo "Proxy: $PROXY_URL"
echo "Relay: $RELAY_HTTP"
# Step 1: Check NIP-11
echo ""
echo "--- Step 1: NIP-11 Info Document ---"
NIP11=$(curl -sf -H "Accept: application/nostr+json" "$PROXY_HTTP/" || true)
if [ -z "$NIP11" ]; then
echo "FAIL: NIP-11 endpoint not responding"
exit 1
fi
echo "$NIP11" | jq .
echo "PASS: NIP-11 document served"
# Step 2: Create invite token
echo ""
echo "--- Step 2: Create Invite Token ---"
# Get a channel UUID from the relay
CHANNEL_ID=$(curl -sf "$RELAY_HTTP/api/channels" -H "X-Pubkey: 0101010101010101010101010101010101010101010101010101010101010101" | jq -r '.[0].id // empty')
if [ -z "$CHANNEL_ID" ]; then
echo "WARN: No channels found in relay. Creating a test channel..."
CHANNEL_ID=$(curl -sf -X POST "$RELAY_HTTP/api/channels" \
-H "Content-Type: application/json" \
-H "X-Pubkey: 0101010101010101010101010101010101010101010101010101010101010101" \
-d '{"name":"proxy-test","channel_type":"stream","visibility":"open"}' | jq -r '.id')
fi
echo "Using channel: $CHANNEL_ID"
INVITE=$(curl -sf -X POST "$PROXY_HTTP/admin/invite" \
-H "Content-Type: application/json" \
-d "{\"channels\": \"$CHANNEL_ID\", \"hours\": 1, \"max_uses\": 10}" || true)
if [ -z "$INVITE" ]; then
echo "FAIL: Could not create invite token"
exit 1
fi
TOKEN=$(echo "$INVITE" | jq -r '.token')
echo "Token: ${TOKEN:0:30}..."
echo "PASS: Invite token created"
# Step 3: Connect and test NIP-42 + kind:40 query
echo ""
echo "--- Step 3: WebSocket Connection Test ---"
echo "Connecting to $PROXY_URL?token=$TOKEN ..."
# Use a timeout approach: send a REQ for kind:40, expect AUTH challenge + EOSE
# websocat with -t (text) and timeout
RESULT=$(echo '["REQ","test-sub",{"kinds":[40],"limit":10}]' | \
timeout 5 websocat -t -1 "$PROXY_URL?token=$TOKEN" 2>/dev/null || true)
if [ -z "$RESULT" ]; then
echo "FAIL: No response from proxy (timeout or connection refused)"
echo "Make sure the proxy is running: just proxy"
exit 1
fi
echo "Response: $RESULT"
# Check if we got an AUTH challenge (expected before we authenticate)
if echo "$RESULT" | grep -q '"AUTH"'; then
echo "PASS: Received NIP-42 AUTH challenge"
else
echo "INFO: Did not receive AUTH challenge in first message"
fi
echo ""
echo "=== Test Summary ==="
echo "NIP-11: PASS"
echo "Invite creation: PASS"
echo "WebSocket connection: PASS"
echo ""
echo "For full interactive testing, use:"
echo " websocat $PROXY_URL?token=$TOKEN"
echo " Then send: [\"AUTH\", <signed-kind-22242-event>]"
echo " Then send: [\"REQ\",\"sub1\",{\"kinds\":[40],\"limit\":10}]"

View File

@@ -1,152 +0,0 @@
#!/usr/bin/env python3
"""
buzz-proxy E2E test using nostr-sdk v0.44 (Python bindings for rust-nostr)
nostr-sdk is the official Rust Nostr SDK with Python/Swift/Flutter bindings.
Second most popular Nostr SDK after nostr-tools, powers native clients.
Tests:
1. NIP-42 authentication (automatic_authentication)
2. Channel discovery (kind:40)
3. Channel metadata (kind:41)
4. Send channel message (kind:42 via EventBuilder.channel_msg)
5. Read messages (kind:42 fetch + round-trip verification)
"""
import asyncio
import json
import time
from datetime import timedelta
import nostr_sdk
RELAY_URL = "ws://localhost:4869"
PRIVKEY_HEX = open("/tmp/guest3_privkey.txt").read().strip()
CHANNEL_EVENT_ID_HEX = "8155f2a8685beb2b4c5ac1796d555ac0b8e8ae1d3a76a7a3f63cce57905cddeb"
results = []
def ok(name, detail=""):
results.append((name, True))
print(f" ✅ {name}" + (f" — {detail}" if detail else ""))
def fail(name, detail=""):
results.append((name, False))
print(f" ❌ {name}" + (f" — {detail}" if detail else ""))
async def main():
print("=" * 64)
print(" nostr-sdk v0.44 (Python/Rust) E2E TEST against buzz-proxy")
print("=" * 64)
keys = nostr_sdk.Keys.parse(PRIVKEY_HEX)
pubkey = keys.public_key().to_hex()
print(f" Relay: {RELAY_URL}")
print(f" Pubkey: {pubkey[:24]}...")
print()
relay_url = nostr_sdk.RelayUrl.parse(RELAY_URL)
channel_eid = nostr_sdk.EventId.parse(CHANNEL_EVENT_ID_HEX)
timeout = timedelta(seconds=10)
signer = nostr_sdk.NostrSigner.keys(keys)
client = nostr_sdk.ClientBuilder().signer(signer).build()
client.automatic_authentication(True)
# ── 1. Connect + NIP-42 Auth ─────────────────────────────────────────
print("[1/5] Connect + NIP-42 Authentication")
try:
await client.add_relay(relay_url)
await client.connect()
await asyncio.sleep(2)
ok("NIP-42 Auth", f"automatic_authentication as {pubkey[:16]}...")
except Exception as e:
fail("NIP-42 Auth", str(e))
return
# ── 2. Channel Discovery (kind:40) ───────────────────────────────────
print("\n[2/5] Channel Discovery (kind:40)")
try:
f40 = nostr_sdk.Filter().kind(nostr_sdk.Kind(40)).limit(100)
events = await client.fetch_events(f40, timeout)
evts = events.to_vec()
if evts:
for e in evts:
content = json.loads(e.content()) if e.content() else {}
name = content.get("name", "?")
print(f" • {name} (id: {e.id().to_hex()[:24]}...)")
ok("Channel Discovery", f"{len(evts)} channel(s)")
else:
fail("Channel Discovery", "no kind:40 events")
except Exception as e:
fail("Channel Discovery", str(e))
# ── 3. Channel Metadata (kind:41) ────────────────────────────────────
print("\n[3/5] Channel Metadata (kind:41)")
try:
f41 = nostr_sdk.Filter().kind(nostr_sdk.Kind(41)).event(channel_eid).limit(5)
meta = await client.fetch_events(f41, timeout)
meta_vec = meta.to_vec()
if meta_vec:
c = json.loads(meta_vec[0].content())
ok("Channel Metadata", f'name="{c.get("name")}", about="{c.get("about","")[:50]}"')
else:
fail("Channel Metadata", "no kind:41 events")
except Exception as e:
fail("Channel Metadata", str(e))
# ── 4. Send Channel Message (kind:42) ────────────────────────────────
print("\n[4/5] Send Channel Message (EventBuilder.channel_msg)")
ts = int(time.time())
msg_text = f"Hello from nostr-sdk Python! 🐍 [{ts}]"
try:
builder = nostr_sdk.EventBuilder.channel_msg(channel_eid, relay_url, msg_text)
output = await client.send_event_builder(builder)
sent_id = output.id.to_hex() # .id is a property, not a method
ok("Send Message", f'"{msg_text}" (id: {sent_id[:16]}...)')
except Exception as e:
fail("Send Message", str(e))
# ── 5. Read Messages + Round-Trip ────────────────────────────────────
print("\n[5/5] Read Messages + Round-Trip Verification")
try:
f42 = nostr_sdk.Filter().kind(nostr_sdk.Kind(42)).event(channel_eid).limit(10)
msgs = await client.fetch_events(f42, timeout)
msg_vec = msgs.to_vec()
found_ours = False
print(f" {len(msg_vec)} message(s):")
for e in msg_vec:
marker = "→ " if "nostr-sdk Python" in e.content() else " "
print(f" {marker}[{e.author().to_hex()[:12]}...] {e.content()[:65]}")
if "nostr-sdk Python" in e.content():
found_ours = True
if found_ours:
ok("Read + Round-Trip", f"our message verified among {len(msg_vec)}")
elif msg_vec:
ok("Read + Round-Trip", f"{len(msg_vec)} messages (ours may be propagating)")
else:
fail("Read + Round-Trip", "no messages")
except Exception as e:
fail("Read + Round-Trip", str(e))
await client.disconnect()
# ── Summary ──────────────────────────────────────────────────────────
print("\n" + "=" * 64)
print(" RESULTS")
print("=" * 64)
all_pass = True
for name, passed in results:
print(f" {'✅ PASS' if passed else '❌ FAIL'} {name}")
if not passed:
all_pass = False
print("=" * 64)
if all_pass:
print(" 🎉 ALL TESTS PASSED — nostr-sdk Python works with buzz-proxy!")
else:
print(" ⚠️ Some tests failed")
print("=" * 64)
asyncio.run(main())

View File

@@ -1,260 +0,0 @@
/**
* buzz-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 buzz-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-buzz-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 buzz-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 buzz-proxy!'
: ' ⚠️ Some tests failed')
console.log('═'.repeat(64))
process.exit(allPass ? 0 : 1)
}
main().catch(e => { console.error('Fatal:', e); process.exit(1) })