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 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gómez
2026-01-16 13:58:15 +01:00
parent 119f737d3d
commit ae46088333
4 changed files with 67 additions and 9 deletions

8
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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) => {

View File

@@ -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<void> {
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();