diff --git a/src/components/nostr/kinds/PublicChatsRenderer.tsx b/src/components/nostr/kinds/PublicChatsRenderer.tsx index 544cb28..6f036fa 100644 --- a/src/components/nostr/kinds/PublicChatsRenderer.tsx +++ b/src/components/nostr/kinds/PublicChatsRenderer.tsx @@ -6,6 +6,7 @@ import { GroupLink } from "../GroupLink"; import eventStore from "@/services/event-store"; import pool from "@/services/relay-pool"; import type { NostrEvent } from "@/types/nostr"; +import { isSafeRelayURL } from "applesauce-core/helpers/relays"; /** * Extract group references from a kind 10009 event @@ -19,9 +20,17 @@ function extractGroups(event: { tags: string[][] }): Array<{ for (const tag of event.tags) { if (tag[0] === "group" && tag[1] && tag[2]) { + // Only include groups with valid relay URLs + const relayUrl = tag[2]; + if (!isSafeRelayURL(relayUrl)) { + console.warn( + `[PublicChatsRenderer] Skipping group with invalid relay URL: ${relayUrl}`, + ); + continue; + } groups.push({ groupId: tag[1], - relayUrl: tag[2], + relayUrl, }); } } diff --git a/src/lib/nip89-helpers.ts b/src/lib/nip89-helpers.ts index 79a0085..c1a8034 100644 --- a/src/lib/nip89-helpers.ts +++ b/src/lib/nip89-helpers.ts @@ -1,5 +1,6 @@ import { NostrEvent } from "@/types/nostr"; import { getTagValue } from "applesauce-core/helpers"; +import { isSafeRelayURL } from "applesauce-core/helpers/relays"; import { AddressPointer } from "nostr-tools/nip19"; /** @@ -229,10 +230,13 @@ export function getHandlerReferences(event: NostrEvent): HandlerReference[] { const relayHint = tag[2]; const platform = tag[3]; + // Only include relay hint if it's a valid websocket URL + const validRelayHint = + relayHint && isSafeRelayURL(relayHint) ? relayHint : undefined; references.push({ address, - relayHint: relayHint || undefined, + relayHint: validRelayHint, platform: platform || undefined, }); } diff --git a/src/lib/nostr-utils.ts b/src/lib/nostr-utils.ts index eefb49d..cc457ab 100644 --- a/src/lib/nostr-utils.ts +++ b/src/lib/nostr-utils.ts @@ -4,6 +4,7 @@ import type { NostrFilter } from "@/types/nostr"; import { getNip10References } from "applesauce-common/helpers/threading"; import { getCommentReplyPointer } from "applesauce-common/helpers/comment"; import type { EventPointer, AddressPointer } from "nostr-tools/nip19"; +import { isSafeRelayURL } from "applesauce-core/helpers/relays"; export function derivePlaceholderName(pubkey: string): string { return `${pubkey.slice(0, 4)}:${pubkey.slice(-4)}`; @@ -81,6 +82,10 @@ export function getEventPointerFromQTag( const relayHint = tag[2]; const authorHint = tag[3]; + // Validate relay hint is a valid websocket URL before using + const validRelayHint = + relayHint && isSafeRelayURL(relayHint) ? relayHint : undefined; + // Check if it's an address coordinate (contains colons: kind:pubkey:d-tag) if (value.includes(":")) { const parts = value.split(":"); @@ -91,8 +96,8 @@ export function getEventPointerFromQTag( if (!isNaN(kind) && pubkey) { const pointer: AddressPointer = { kind, pubkey, identifier }; - if (relayHint) { - pointer.relays = [relayHint]; + if (validRelayHint) { + pointer.relays = [validRelayHint]; } return pointer; } @@ -106,8 +111,8 @@ export function getEventPointerFromQTag( } const pointer: EventPointer = { id: value }; - if (relayHint) { - pointer.relays = [relayHint]; + if (validRelayHint) { + pointer.relays = [validRelayHint]; } if (authorHint && /^[0-9a-f]{64}$/i.test(authorHint)) { pointer.author = authorHint; diff --git a/src/lib/relay-url.ts b/src/lib/relay-url.ts index 413cad1..18c9089 100644 --- a/src/lib/relay-url.ts +++ b/src/lib/relay-url.ts @@ -1,10 +1,17 @@ import { normalizeURL as applesauceNormalizeURL } from "applesauce-core/helpers"; +import { isSafeRelayURL } from "applesauce-core/helpers/relays"; + +// Re-export applesauce's fast relay URL check for use in hot paths +export { isSafeRelayURL }; /** * Check if a string is a valid relay URL * - Must have ws:// or wss:// protocol * - Must be a valid URL structure - * - Must not contain invalid characters + * - Must have a valid hostname + * + * Uses applesauce's isSafeRelayURL for fast validation of domain-based URLs, + * with a fallback to URL constructor for IP addresses (which isSafeRelayURL doesn't support). * * @returns true if the URL is a valid relay URL, false otherwise */ @@ -21,8 +28,14 @@ export function isValidRelayURL(url: unknown): url is string { return false; } + // Fast path: use applesauce's regex-based validation for domain URLs + if (isSafeRelayURL(trimmed)) { + return true; + } + + // Fallback: use URL constructor for IP addresses and edge cases + // isSafeRelayURL doesn't support IP addresses like 192.168.1.1 or 127.0.0.1 try { - // Must be a valid URL structure const parsed = new URL(trimmed); // Protocol must be ws: or wss: diff --git a/src/lib/zapstore-helpers.ts b/src/lib/zapstore-helpers.ts index aa3f2b1..ffb8480 100644 --- a/src/lib/zapstore-helpers.ts +++ b/src/lib/zapstore-helpers.ts @@ -1,5 +1,6 @@ import { NostrEvent } from "@/types/nostr"; import { getTagValue } from "applesauce-core/helpers"; +import { isSafeRelayURL } from "applesauce-core/helpers/relays"; import { AddressPointer } from "nostr-tools/nip19"; /** @@ -227,9 +228,12 @@ export function getAppReferences(event: NostrEvent): AppReference[] { // Kind 32267 apps are expected in curation sets if (address.kind === 32267) { const relayHint = tag[2]; + // Only include relay hint if it's a valid websocket URL + const validRelayHint = + relayHint && isSafeRelayURL(relayHint) ? relayHint : undefined; references.push({ address, - relayHint: relayHint || undefined, + relayHint: validRelayHint, }); } } diff --git a/src/services/loaders.ts b/src/services/loaders.ts index 75db474..d3cd7f4 100644 --- a/src/services/loaders.ts +++ b/src/services/loaders.ts @@ -6,7 +6,11 @@ import { } from "applesauce-loaders/loaders"; import type { EventPointer } from "nostr-tools/nip19"; import { Observable } from "rxjs"; -import { getSeenRelays, mergeRelaySets } from "applesauce-core/helpers/relays"; +import { + getSeenRelays, + mergeRelaySets, + isSafeRelayURL, +} from "applesauce-core/helpers/relays"; import { getEventPointerFromETag, getAddressPointerFromATag, @@ -16,7 +20,6 @@ import pool from "./relay-pool"; import eventStore from "./event-store"; import { relayListCache } from "./relay-list-cache"; import type { NostrEvent } from "@/types/nostr"; -import { isValidRelayURL } from "@/lib/relay-url"; /** * Extract relay context from a Nostr event for comprehensive relay selection @@ -37,7 +40,9 @@ function extractRelayContext(event: NostrEvent): { const rTags = event.tags .filter((t) => t[0] === "r") .map((t) => t[1]) - .filter(isValidRelayURL); + .filter( + (url): url is string => typeof url === "string" && isSafeRelayURL(url), + ); // Extract relay hints from all "e" tags using applesauce helper // Filter to only valid relay URLs @@ -48,7 +53,9 @@ function extractRelayContext(event: NostrEvent): { // v5: returns null for invalid tags instead of throwing return pointer?.relays?.[0]; // First relay hint from the pointer }) - .filter(isValidRelayURL); + .filter( + (url): url is string => typeof url === "string" && isSafeRelayURL(url), + ); // Extract relay hints from all "a" tags (addressable event references) // This includes both lowercase "a" (reply) and uppercase "A" (root) tags @@ -59,7 +66,9 @@ function extractRelayContext(event: NostrEvent): { const pointer = getAddressPointerFromATag(tag); return pointer?.relays?.[0]; // First relay hint from the pointer }) - .filter(isValidRelayURL); + .filter( + (url): url is string => typeof url === "string" && isSafeRelayURL(url), + ); // Extract first "p" tag as author hint using applesauce helper const authorHint = getTagValue(event, "p");