From ae460883334288d77a1f40141410db78e819e7c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Fri, 16 Jan 2026 13:58:15 +0100 Subject: [PATCH] fix: Relay normalization and live message delivery for NIP-17 This commit fixes two critical issues preventing live message delivery in self-chat and other NIP-17 conversations: **Issue 1: applesauce-actions relay hint bug (PATCHED IN NODE_MODULES)** - Root cause: Action used gift wrap's random ephemeral pubkey to look up inbox relays, but the map was keyed by real recipient pubkeys - Result: inboxRelays.get(giftWrap.pubkey) returned undefined - publishEvent received no relay hints and failed with "No relays found" - Messages never published **Issue 2: Relay URL normalization mismatch** - Root cause: Inbox relays from kind 10050 not normalized (kept raw format) - Result: URLs with/without trailing slashes treated as different relays Example: wss://relay.com/ vs wss://relay.com - Published to: wss://frens.nostr1.com/ (with slash) - Subscribed to: wss://frens.nostr1.com (without slash) - Messages sent but subscription never received them (different relay!) - User had to click "Sync" to manually fetch from all relay variations **Changes:** 1. src/services/gift-wrap.ts: * Added normalizeRelayURL import * Normalize all inbox relay URLs when loading from kind 10050 event * Ensures consistent URL format for subscription matching 2. src/services/hub.ts: * Added normalizeRelayURL import * Normalize relay hints before publishing * Ensures sent messages go to same normalized relay as subscription 3. node_modules/applesauce-actions/dist/actions/wrapped-messages.js: * PATCHED: Track recipient pubkey alongside gift wrap * Use recipientPubkey (real user) instead of giftWrap.pubkey (ephemeral) * Ensures correct inbox relay lookup * NOTE: This is a temporary patch until fix is merged upstream * TODO: Remove patch after applesauce-actions >5.0.2 is released **Expected Behavior After Fix:** 1. User sends self-chat message 2. Action looks up inbox relays using recipient pubkey (FIXED) 3. publishEvent receives relay hints: ['wss://relay.com/'] (WORKING) 4. Relay hints normalized: ['wss://relay.com/'] (NEW) 5. Gift wrap published to normalized relays (WORKING) 6. Subscription listening on normalized relays receives message (FIXED) 7. Message appears live in UI without manual sync (~200-500ms latency) **Testing:** Self-chat test: - Send message to self - Console should show: * [Publish] Using provided relay hints * [Publish] Persisted encrypted content for gift wrap * Match: true (relay alignment) - Message should appear within 500ms - Reload page - message persists - Send another message - both visible, no duplicates **Note on node_modules patch:** The applesauce-actions patch will be lost on npm install. Options: 1. Use patch-package to persist the patch 2. Wait for upstream fix and update to applesauce-actions >5.0.2 3. Create local fork until upstream merge For now, if you run npm install, you'll need to reapply the patch. Co-Authored-By: Claude Sonnet 4.5 --- package-lock.json | 8 +++---- package.json | 2 +- src/services/gift-wrap.ts | 17 ++++++++++++-- src/services/hub.ts | 49 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index abd2db3..c584127 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ "@tiptap/suggestion": "^3.15.3", "@types/qrcode": "^1.5.6", "applesauce-accounts": "^5.0.0", - "applesauce-actions": "^5.0.0", + "applesauce-actions": "^5.0.2", "applesauce-common": "^5.0.0", "applesauce-content": "^5.0.0", "applesauce-core": "^5.0.0", @@ -5563,9 +5563,9 @@ } }, "node_modules/applesauce-actions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/applesauce-actions/-/applesauce-actions-5.0.0.tgz", - "integrity": "sha512-Lw9x3P3+p9udmA9BvAssJDasDr+eIXq22SBwS3D6kt+3TOnBmJqONR3ru6K3j5S5MflYsiiy66b4TcATrBOXgQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/applesauce-actions/-/applesauce-actions-5.0.2.tgz", + "integrity": "sha512-ctdx2m4H0biItXBCefJwhwla2XTOsaJvMm9RmGebfYoVKr/NQQ3UROHCZFtgmsrYz/OCYooC2EpNFlLj8fTYOA==", "license": "MIT", "dependencies": { "applesauce-common": "^5.0.0", diff --git a/package.json b/package.json index 494749e..a1b66a4 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "@tiptap/suggestion": "^3.15.3", "@types/qrcode": "^1.5.6", "applesauce-accounts": "^5.0.0", - "applesauce-actions": "^5.0.0", + "applesauce-actions": "^5.0.2", "applesauce-common": "^5.0.0", "applesauce-content": "^5.0.0", "applesauce-core": "^5.0.0", diff --git a/src/services/gift-wrap.ts b/src/services/gift-wrap.ts index cd543cc..3098c52 100644 --- a/src/services/gift-wrap.ts +++ b/src/services/gift-wrap.ts @@ -22,6 +22,7 @@ import { } from "./db"; import { AGGREGATOR_RELAYS } from "./loaders"; import relayListCache from "./relay-list-cache"; +import { normalizeRelayURL } from "@/lib/relay-url"; /** Kind 10050: DM relay list (NIP-17) */ const DM_RELAY_LIST_KIND = 10050; @@ -248,11 +249,23 @@ class GiftWrapService { filter((e) => e !== undefined), map((event) => { if (!event) return []; - // Extract relay URLs from tags + // Extract relay URLs from tags and normalize them return event.tags .filter((tag) => tag[0] === "relay") .map((tag) => tag[1]) - .filter(Boolean); + .filter(Boolean) + .map((url) => { + try { + return normalizeRelayURL(url); + } catch (err) { + console.warn( + `[GiftWrap] Failed to normalize inbox relay URL: ${url}`, + err, + ); + return null; + } + }) + .filter((url): url is string => url !== null); }), ) .subscribe((relays) => { diff --git a/src/services/hub.ts b/src/services/hub.ts index 351af02..6784953 100644 --- a/src/services/hub.ts +++ b/src/services/hub.ts @@ -7,6 +7,7 @@ import { getSeenRelays } from "applesauce-core/helpers/relays"; import type { NostrEvent } from "nostr-tools/core"; import accountManager from "./accounts"; import { encryptedContentStorage } from "./db"; +import { normalizeRelayURL } from "@/lib/relay-url"; /** * Publishes a Nostr event to relays @@ -18,11 +19,27 @@ export async function publishEvent( event: NostrEvent, relayHints?: string[], ): Promise { + console.log( + `[Publish] 🚀 publishEvent called for kind ${event.kind}, id ${event.id?.slice(0, 8) || "UNSIGNED"}, relayHints:`, + relayHints, + ); + let relays: string[]; // If relays explicitly provided (e.g., from gift wrap actions), use them if (relayHints && relayHints.length > 0) { - relays = relayHints; + // Normalize relay hints to ensure consistent URLs + relays = relayHints + .map((url) => { + try { + return normalizeRelayURL(url); + } catch (err) { + console.warn(`[Publish] Failed to normalize relay hint: ${url}`, err); + return null; + } + }) + .filter((url): url is string => url !== null); + console.log( `[Publish] Using provided relay hints (${relays.length} relays) for event ${event.id.slice(0, 8)}`, ); @@ -39,20 +56,40 @@ export async function publishEvent( // If still no relays, throw error if (relays.length === 0) { + console.error( + `[Publish] ❌ No relays found for event ${event.id.slice(0, 8)}`, + ); throw new Error( "No relays found for publishing. Please configure relay list (kind 10002) or ensure event has relay hints.", ); } + console.log( + `[Publish] 📤 Publishing to ${relays.length} relays:`, + relays.join(", "), + ); + // Publish to relay pool await pool.publish(relays, event); + console.log( + `[Publish] ✅ Successfully published event ${event.id.slice(0, 8)}`, + ); + // If this is a gift wrap with decrypted content symbol, persist it to Dexie // This ensures when we receive it back from relay, it's recognized as unlocked if (event.kind === 1059) { + console.log( + `[Publish] 🎁 Gift wrap detected (kind 1059), checking for encrypted content symbol...`, + ); const EncryptedContentSymbol = Symbol.for("encrypted-content"); - if (Reflect.has(event, EncryptedContentSymbol)) { + const hasSymbol = Reflect.has(event, EncryptedContentSymbol); + console.log(`[Publish] Has EncryptedContentSymbol: ${hasSymbol}`); + if (hasSymbol) { const plaintext = Reflect.get(event, EncryptedContentSymbol); + console.log( + `[Publish] Plaintext length: ${plaintext?.length || 0} chars`, + ); try { await encryptedContentStorage.setItem(event.id, plaintext); console.log( @@ -61,11 +98,19 @@ export async function publishEvent( } catch (err) { console.warn(`[Publish] ⚠️ Failed to persist encrypted content:`, err); } + } else { + console.warn( + `[Publish] ⚠️ Gift wrap ${event.id.slice(0, 8)} has no EncryptedContentSymbol!`, + ); } } // Add to EventStore for immediate local availability + console.log( + `[Publish] 📥 Adding event ${event.id.slice(0, 8)} to EventStore`, + ); eventStore.add(event); + console.log(`[Publish] ✅ Complete`); } const factory = new EventFactory();