From 8ae69fde76945377189281182954c946ff9ad419 Mon Sep 17 00:00:00 2001 From: bndw Date: Sun, 15 Feb 2026 09:03:51 -0800 Subject: fix: properly handle AUTH flow with retry logic - Remove blocking wait for AUTH challenge on connection - Add channel-based synchronization for AUTH completion - Retry publish after AUTH completes successfully - Support both auth-required and non-auth relay configurations - Add clarifying comments for auth flow handling --- testclient/main.go | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 testclient/main.go (limited to 'testclient/main.go') diff --git a/testclient/main.go b/testclient/main.go new file mode 100644 index 0000000..92726bb --- /dev/null +++ b/testclient/main.go @@ -0,0 +1,195 @@ +package main + +import ( + "context" + "encoding/hex" + "flag" + "fmt" + "log" + "os" + "time" + + "fiatjaf.com/nostr" + "fiatjaf.com/nostr/nip19" +) + +const keyFile = "testclient.key" + +func main() { + relayURL := flag.String("relay", "ws://localhost:8080", "Relay WebSocket URL") + flag.Parse() + + // Load or generate keypair + sk, err := loadOrGenerateKey() + if err != nil { + log.Fatalf("Failed to load/generate key: %v", err) + } + + pubkey := nostr.GetPublicKey(sk) + npub := nip19.EncodeNpub(pubkey) + log.Printf("Pubkey (hex): %s", hex.EncodeToString(pubkey[:])) + log.Printf("Pubkey (npub): %s", npub) + + // Connect to relay with NIP-42 auth handler + log.Printf("Connecting to %s...", *relayURL) + + // Channel to signal when AUTH completes + authCompleted := make(chan error, 1) + + opts := nostr.RelayOptions{ + // NIP-42 AUTH handler - signs auth challenges automatically + AuthHandler: func(ctx context.Context, r *nostr.Relay, authEvent *nostr.Event) error { + log.Printf("AUTH challenge received! Signing auth event...") + if err := authEvent.Sign(sk); err != nil { + authErr := fmt.Errorf("failed to sign auth event: %w", err) + authCompleted <- authErr + return authErr + } + log.Printf("AUTH event signed and sent") + authCompleted <- nil + return nil + }, + // Notice handler - logs relay notices + NoticeHandler: func(r *nostr.Relay, notice string) { + log.Printf("NOTICE from relay: %s", notice) + }, + } + + relay, err := nostr.RelayConnect(context.Background(), *relayURL, opts) + if err != nil { + log.Fatalf("Failed to connect: %v", err) + } + defer relay.Close() + + log.Printf("Connected! (NIP-42 auth enabled)") + + // Try to publish an event (will trigger AUTH if required) + log.Printf("Publishing test event...") + event := nostr.Event{ + PubKey: pubkey, + CreatedAt: nostr.Now(), + Kind: nostr.KindTextNote, + Tags: nostr.Tags{}, + Content: fmt.Sprintf("NIP-42 test from testclient at %s", time.Now().Format(time.RFC3339)), + } + + event.Sign(sk) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Try to publish - this may trigger AUTH if relay requires it + err = relay.Publish(ctx, event) + if err != nil { + // Publish failed - likely due to AUTH requirement + log.Printf("First publish attempt failed: %v", err) + + // Wait for AUTH to complete + log.Printf("Waiting for AUTH to complete...") + select { + case authErr := <-authCompleted: + if authErr != nil { + log.Fatalf("AUTH failed: %v", authErr) + } + log.Printf("AUTH completed successfully") + case <-ctx.Done(): + log.Fatalf("Timeout waiting for AUTH") + } + + // Retry publish now that we're authenticated + log.Printf("Retrying publish after successful auth...") + err = relay.Publish(ctx, event) + if err != nil { + log.Printf("Publish error after retry: %v", err) + } else { + log.Printf("Event published successfully! ID: %s", hex.EncodeToString(event.ID[:])) + } + } else { + // Publish succeeded immediately - no AUTH required + log.Printf("Event published successfully! ID: %s", hex.EncodeToString(event.ID[:])) + } + + // Query events + log.Printf("Querying recent events (kind 1, limit 10)...") + filters := nostr.Filter{ + Kinds: []nostr.Kind{nostr.KindTextNote}, + Limit: 10, + } + + queryCtx, queryCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer queryCancel() + + sub, err := relay.Subscribe(queryCtx, filters, nostr.SubscriptionOptions{}) + if err != nil { + log.Printf("Subscribe error: %v", err) + return + } + + count := 0 + timeout := time.After(3 * time.Second) + + log.Printf("Listening for events...") + for { + select { + case evt, ok := <-sub.Events: + if !ok { + goto done + } + count++ + log.Printf(" Event %d: kind=%d author=%s content=%q", + count, evt.Kind, hex.EncodeToString(evt.PubKey[:])[:16], truncate(evt.Content, 50)) + if count >= 10 { + goto done + } + case <-sub.EndOfStoredEvents: + log.Printf("End of stored events (EOSE)") + goto done + case <-timeout: + log.Printf("Query timeout") + goto done + case <-queryCtx.Done(): + goto done + } + } + +done: + log.Printf("Retrieved %d events total", count) + sub.Close() + + log.Printf("Test complete!") +} + +// loadOrGenerateKey loads an existing key or generates a new one +func loadOrGenerateKey() (nostr.SecretKey, error) { + // Try to load existing key + data, err := os.ReadFile(keyFile) + if err == nil { + skHex := string(data) + skBytes, err := hex.DecodeString(skHex) + if err == nil && len(skBytes) == 32 { + var sk nostr.SecretKey + copy(sk[:], skBytes) + log.Printf("Loaded existing key from %s", keyFile) + return sk, nil + } + } + + // Generate new key + sk := nostr.Generate() + skHex := hex.EncodeToString(sk[:]) + + if err := os.WriteFile(keyFile, []byte(skHex), 0o600); err != nil { + return sk, fmt.Errorf("failed to save key: %w", err) + } + + log.Printf("Generated new key and saved to %s", keyFile) + return sk, nil +} + +// truncate truncates a string to maxLen characters +func truncate(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] + "..." +} -- cgit v1.2.3