diff --git a/package-lock.json b/package-lock.json index 5f3c1d06..c909df4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "imwald", - "version": "23.15.1", + "version": "23.16.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "imwald", - "version": "23.15.1", + "version": "23.16.1", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", diff --git a/package.json b/package.json index e2af0d6e..ab78c749 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imwald", - "version": "23.16.0", + "version": "23.16.1", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "private": true, "type": "module", diff --git a/src/components/MailboxSetting/DiscoveredRelays.tsx b/src/components/MailboxSetting/DiscoveredRelays.tsx index ddad9fdb..ce0ebcb6 100644 --- a/src/components/MailboxSetting/DiscoveredRelays.tsx +++ b/src/components/MailboxSetting/DiscoveredRelays.tsx @@ -1,7 +1,7 @@ import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { Checkbox } from '@/components/ui/checkbox' -import { isLocalNetworkUrl, normalizeHttpRelayUrl } from '@/lib/url' +import { isLocalNetworkUrl, isWebsocketUrl, normalizeRelayUrlByScheme } from '@/lib/url' import { getRelaysFromNip07Extension, verifyNip05 } from '@/lib/nip05' import { useNostr } from '@/providers/NostrProvider' import { TMailboxRelay } from '@/types' @@ -44,8 +44,8 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd: const nip05Result = await verifyNip05(profile.nip05, account.pubkey) if (nip05Result.isVerified && nip05Result.relays) { nip05Result.relays.forEach(url => { - const normalized = normalizeHttpRelayUrl(url) - if (normalized && !discovered.has(normalized)) { + const normalized = normalizeRelayUrlByScheme(url) + if (normalized && isWebsocketUrl(normalized) && !discovered.has(normalized)) { discovered.set(normalized, { url: normalized, source: 'nip05', @@ -64,8 +64,8 @@ export default function DiscoveredRelays({ onAdd, localOnly = false }: { onAdd: try { const extensionRelays = await getRelaysFromNip07Extension() extensionRelays.forEach(url => { - const normalized = normalizeHttpRelayUrl(url) - if (normalized && !discovered.has(normalized)) { + const normalized = normalizeRelayUrlByScheme(url) + if (normalized && isWebsocketUrl(normalized) && !discovered.has(normalized)) { discovered.set(normalized, { url: normalized, source: 'nip07', diff --git a/src/components/MailboxSetting/index.tsx b/src/components/MailboxSetting/index.tsx index abfa3742..6eb5abd8 100644 --- a/src/components/MailboxSetting/index.tsx +++ b/src/components/MailboxSetting/index.tsx @@ -1,5 +1,5 @@ import { Button } from '@/components/ui/button' -import { isLocalNetworkUrl, normalizeHttpRelayUrl } from '@/lib/url' +import { isLocalNetworkUrl, normalizeAnyRelayUrl } from '@/lib/url' import { useNostr } from '@/providers/NostrProvider' import { TMailboxRelay, TMailboxRelayScope } from '@/types' import { useEffect, useState } from 'react' @@ -98,7 +98,7 @@ export default function MailboxSetting() { const saveNewMailboxRelay = (url: string) => { if (url === '') return null - const normalizedUrl = normalizeHttpRelayUrl(url) + const normalizedUrl = normalizeAnyRelayUrl(url) if (!normalizedUrl) { return t('Invalid relay URL') } diff --git a/src/lib/pre-publish-relay-cap.ts b/src/lib/pre-publish-relay-cap.ts index d00f49a0..78be21ee 100644 --- a/src/lib/pre-publish-relay-cap.ts +++ b/src/lib/pre-publish-relay-cap.ts @@ -2,7 +2,7 @@ import { isSocialKindBlockedKind, MAX_PUBLISH_RELAYS, SOCIAL_KIND_BLOCKED_RELAY_ import { kinds } from 'nostr-tools' import { filterRelaysForEventPublish } from '@/lib/relay-publish-filter' import { dedupeNormalizeRelayUrlsOrdered } from '@/lib/relay-url-priority' -import { normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url' +import { normalizeHttpRelayUrl, normalizeRelayUrlByScheme, normalizeUrl } from '@/lib/url' import type { NostrEvent } from 'nostr-tools' export type TPrePublishRelayCapPreview = { @@ -50,7 +50,7 @@ export function computePrePublishRelayCapPreview({ const socialBlockedSet = new Set(SOCIAL_KIND_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u)) outbox = dedupeNormalizeRelayUrlsOrdered( filterRelaysForEventPublish(outbox, previewKind).filter((url) => { - const n = normalizeAnyRelayUrl(url) || url + const n = normalizeRelayUrlByScheme(url) || url if (applySocialOutboxFilter && socialBlockedSet.has(n)) return false return true }) @@ -64,7 +64,7 @@ export function computePrePublishRelayCapPreview({ const outboxNormSet = new Set(outbox) const outboxSlotsInPublish = selectedRelayUrls.length > 0 ? 0 : capped.filter((u) => outboxNormSet.has(u)).length - const selectedNorm = selectedRelayUrls.map((u) => normalizeAnyRelayUrl(u) || u) + const selectedNorm = selectedRelayUrls.map((u) => normalizeRelayUrlByScheme(u) || u) const selectedContacted = selectedNorm.filter((u) => capped.includes(u)).length const showCapHint = diff --git a/src/lib/relay-list-sanitize.ts b/src/lib/relay-list-sanitize.ts index c05ea3ab..0ee486bb 100644 --- a/src/lib/relay-list-sanitize.ts +++ b/src/lib/relay-list-sanitize.ts @@ -1,4 +1,10 @@ -import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url' +import { + isLocalNetworkUrl, + normalizeAnyRelayUrl, + normalizeHttpRelayUrl, + normalizeRelayUrlByScheme, + normalizeUrl +} from '@/lib/url' import type { TMailboxRelay, TMailboxRelayScope, TRelayList } from '@/types' /** True if this URL is not loopback / LAN (safe to open from another user's browser as a REQ target). */ @@ -81,13 +87,7 @@ export function stripLocalNetworkRelaysForWssReq(urls: readonly string[]): strin return out } -const normRelayKey = (u: string): string => { - const t = typeof u === 'string' ? u.trim() : '' - if (!t) return '' - if (/^wss?:\/\//i.test(t)) return normalizeUrl(t) || t - if (/^https?:\/\//i.test(t)) return normalizeHttpRelayUrl(t) || t - return normalizeUrl(t) || normalizeHttpRelayUrl(t) || t -} +const normRelayKey = (u: string): string => normalizeRelayUrlByScheme(u) || u.trim() /** * When NIP-65 `originalRelays` is empty but `read` / `write` URL lists are filled (e.g. PROFILE_FETCH fallback), diff --git a/src/lib/relay-url-normalize.test.ts b/src/lib/relay-url-normalize.test.ts index d05e2206..127c4ce7 100644 --- a/src/lib/relay-url-normalize.test.ts +++ b/src/lib/relay-url-normalize.test.ts @@ -5,6 +5,7 @@ import { httpIndexRelayBasesInUrlBatch, normalizeAnyRelayUrl, normalizeHttpRelayUrl, + normalizeRelayUrlByScheme, normalizeRelayUrlForPage, normalizeUrl } from '@/lib/url' @@ -27,6 +28,14 @@ describe('relay URL normalization', () => { expect(normalizeUrl('wss://nostr.land/')).toMatch(/^wss:\/\/nostr\.land\/?$/) }) + it('normalizeRelayUrlByScheme routes by scheme', () => { + expect(normalizeRelayUrlByScheme('wss://nostr.land/')).toMatch(/^wss:\/\/nostr\.land\/?$/) + expect(normalizeRelayUrlByScheme('https://mercury-relay.imwald.eu/')).toMatch( + /^https:\/\/mercury-relay\.imwald\.eu\/?$/ + ) + expect(normalizeRelayUrlByScheme('mercury-relay.imwald.eu')).toBe('') + }) + it('rejects bare hostnames', () => { expect(normalizeAnyRelayUrl('mercury-relay.imwald.eu')).toBe('') expect(normalizeAnyRelayUrl('nostr.land')).toBe('') diff --git a/src/lib/relay-url-priority.ts b/src/lib/relay-url-priority.ts index e26583ef..9f0124b8 100644 --- a/src/lib/relay-url-priority.ts +++ b/src/lib/relay-url-priority.ts @@ -5,7 +5,7 @@ import { MAX_REQ_RELAY_URLS } from '@/constants' import { feedRelayPolicyUrls, type FeedRelayLayer } from '@/features/feed/relay-policy' -import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url' +import { isLocalNetworkUrl, normalizeAnyRelayUrl, normalizeRelayUrlByScheme, normalizeUrl } from '@/lib/url' export { MAX_REQ_RELAY_URLS } @@ -13,7 +13,7 @@ export function dedupeNormalizeRelayUrlsOrdered(urls: readonly string[]): string const seen = new Set() const out: string[] = [] for (const u of urls) { - const n = normalizeAnyRelayUrl(u) || u.trim() + const n = normalizeRelayUrlByScheme(u) || u.trim() if (!n || seen.has(n)) continue seen.add(n) out.push(n) diff --git a/src/lib/url.ts b/src/lib/url.ts index 1a0ed4b2..cc0f5c5d 100644 --- a/src/lib/url.ts +++ b/src/lib/url.ts @@ -118,9 +118,20 @@ export function normalizeAnyRelayUrl(url: string): string { return normalizeUrl(url) } +/** + * Normalize a relay URL using the route for its scheme: `http(s)` index relays (kind 10243) + * vs `ws(s)` NIP-01 relays (kind 10002). Bare hostnames are rejected by both routes. + */ +export function normalizeRelayUrlByScheme(url: string): string { + const trimmed = url.trim() + if (!trimmed) return '' + if (isHttpOrHttpsScheme(trimmed)) return normalizeHttpRelayUrl(trimmed) + return normalizeAnyRelayUrl(trimmed) +} + /** Relay explore/detail routes accept WebSocket relays or kind-10243 HTTP index bases. */ export function normalizeRelayUrlForPage(url: string): string { - return normalizeAnyRelayUrl(url) || normalizeHttpRelayUrl(url) + return normalizeRelayUrlByScheme(url) } /** Stable key for per-relay session stats (scheme preserved; no https→wss aliasing). */ diff --git a/src/services/relay-selection.service.ts b/src/services/relay-selection.service.ts index a850dc4f..0eacddcb 100644 --- a/src/services/relay-selection.service.ts +++ b/src/services/relay-selection.service.ts @@ -11,10 +11,9 @@ import client from '@/services/client.service' import { eventService } from '@/services/client.service' import { canonicalRelaySessionKey, - isHttpOrHttpsScheme, isLocalNetworkUrl, normalizeAnyRelayUrl, - normalizeHttpRelayUrl + normalizeRelayUrlByScheme } from '@/lib/url' import { TRelaySet, TRelayList } from '@/types' import logger from '@/lib/logger' @@ -144,9 +143,7 @@ class RelaySelectionService { const addRelay = (url: string, type: RelaySourceType) => { if (!url) return - const normalized = isHttpOrHttpsScheme(url) - ? normalizeHttpRelayUrl(url) - : normalizeAnyRelayUrl(url) + const normalized = normalizeRelayUrlByScheme(url) const key = normalized ? canonicalRelaySessionKey(normalized) : '' if (key && !seen.has(key)) { seen.add(key)