From b5e854a573a0830e95364cfe3b4d66d2914f7d77 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 9 Jul 2025 19:38:19 +0200 Subject: [PATCH] Double-checked relay implementation and added status page and better logging --- src/lib/components/RelayStatus.svelte | 162 +++++++++++++ src/lib/consts.ts | 8 + src/lib/ndk.ts | 318 ++++++++++++++++++++++++-- src/lib/utils/nostrUtils.ts | 12 +- src/routes/about/+page.svelte | 5 + 5 files changed, 478 insertions(+), 27 deletions(-) create mode 100644 src/lib/components/RelayStatus.svelte diff --git a/src/lib/components/RelayStatus.svelte b/src/lib/components/RelayStatus.svelte new file mode 100644 index 0000000..51bfb1d --- /dev/null +++ b/src/lib/components/RelayStatus.svelte @@ -0,0 +1,162 @@ + + +
+
+

Relay Connection Status

+ +
+ + {#if !$ndkSignedIn} + + Anonymous Mode +

+ You are not signed in. Some relays require authentication and may not be accessible. + Sign in to access all relays. +

+
+ {/if} + +
+ {#each relayStatuses as status} +
+
+
{status.url}
+
+ {getStatusText(status)} +
+
+
+
+ {/each} +
+ + {#if relayStatuses.some(s => s.requiresAuth && !$ndkSignedIn)} + + Authentication Required +

+ Some relays require authentication. Sign in to access these relays. +

+
+ {/if} +
\ No newline at end of file diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 4f06e91..034f8d2 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -10,6 +10,14 @@ export const standardRelays = [ //'wss://thecitadel.gitcitadel.eu', //'wss://theforest.gitcitadel.eu', ]; + +// Non-auth relays for anonymous users +export const anonymousRelays = [ + 'wss://thecitadel.nostr1.com', + 'wss://theforest.nostr1.com', + 'wss://profiles.nostr1.com', + 'wss://freelay.sovbit.host', +]; export const fallbackRelays = [ 'wss://purplepag.es', 'wss://indexer.coracle.social', diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts index d5a0f79..a518ff4 100644 --- a/src/lib/ndk.ts +++ b/src/lib/ndk.ts @@ -1,6 +1,6 @@ -import NDK, { NDKNip07Signer, NDKRelay, NDKRelayAuthPolicies, NDKRelaySet, NDKUser } from '@nostr-dev-kit/ndk'; +import NDK, { NDKNip07Signer, NDKRelay, NDKRelayAuthPolicies, NDKRelaySet, NDKUser, NDKEvent } from '@nostr-dev-kit/ndk'; import { get, writable, type Writable } from 'svelte/store'; -import { fallbackRelays, FeedType, loginStorageKey, standardRelays } from './consts'; +import { fallbackRelays, FeedType, loginStorageKey, standardRelays, anonymousRelays } from './consts'; import { feedType } from './stores'; export const ndkInstance: Writable = writable(); @@ -12,6 +12,237 @@ export const activePubkey: Writable = writable(null); export const inboxRelays: Writable = writable([]); export const outboxRelays: Writable = writable([]); +/** + * Custom authentication policy that handles NIP-42 authentication manually + * when the default NDK authentication fails + */ +class CustomRelayAuthPolicy { + private ndk: NDK; + private challenges: Map = new Map(); + + constructor(ndk: NDK) { + this.ndk = ndk; + } + + /** + * Handles authentication for a relay + * @param relay The relay to authenticate with + * @returns Promise that resolves when authentication is complete + */ + async authenticate(relay: NDKRelay): Promise { + if (!this.ndk.signer || !this.ndk.activeUser) { + console.warn('[NDK.ts] No signer or active user available for relay authentication'); + return; + } + + try { + console.debug(`[NDK.ts] Setting up authentication for ${relay.url}`); + + // Listen for AUTH challenges + relay.on('auth', (challenge: string) => { + console.debug(`[NDK.ts] Received AUTH challenge from ${relay.url}:`, challenge); + this.challenges.set(relay.url, challenge); + this.handleAuthChallenge(relay, challenge); + }); + + // Listen for auth-required errors (handle via notice events) + relay.on('notice', (message: string) => { + if (message.includes('auth-required')) { + console.debug(`[NDK.ts] Auth required from ${relay.url}:`, message); + this.handleAuthRequired(relay, message); + } + }); + + // Listen for successful authentication + relay.on('authed', () => { + console.debug(`[NDK.ts] Successfully authenticated to ${relay.url}`); + }); + + // Listen for authentication failures + relay.on('auth:failed', (error: any) => { + console.error(`[NDK.ts] Authentication failed for ${relay.url}:`, error); + }); + + } catch (error) { + console.error(`[NDK.ts] Error setting up authentication for ${relay.url}:`, error); + } + } + + /** + * Handles AUTH challenge from relay + */ + private async handleAuthChallenge(relay: NDKRelay, challenge: string): Promise { + try { + if (!this.ndk.signer || !this.ndk.activeUser) { + console.warn('[NDK.ts] No signer available for AUTH challenge'); + return; + } + + // Create NIP-42 authentication event + const authEvent = { + kind: 22242, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['relay', relay.url], + ['challenge', challenge] + ], + content: '', + pubkey: this.ndk.activeUser.pubkey + }; + + // Create and sign the authentication event using NDKEvent + const authNDKEvent = new NDKEvent(this.ndk, authEvent); + await authNDKEvent.sign(); + + // Send AUTH message to relay using the relay's publish method + await relay.publish(authNDKEvent); + console.debug(`[NDK.ts] Sent AUTH to ${relay.url}`); + + } catch (error) { + console.error(`[NDK.ts] Error handling AUTH challenge for ${relay.url}:`, error); + } + } + + /** + * Handles auth-required error from relay + */ + private async handleAuthRequired(relay: NDKRelay, message: string): Promise { + const challenge = this.challenges.get(relay.url); + if (challenge) { + await this.handleAuthChallenge(relay, challenge); + } else { + console.warn(`[NDK.ts] Auth required from ${relay.url} but no challenge available`); + } + } +} + +/** + * Checks if the current environment might cause WebSocket protocol downgrade + */ +export function checkEnvironmentForWebSocketDowngrade(): void { + console.debug('[NDK.ts] Environment Check for WebSocket Protocol:'); + + const isLocalhost = window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1'; + const isHttp = window.location.protocol === 'http:'; + const isHttps = window.location.protocol === 'https:'; + + console.debug('[NDK.ts] - Is localhost:', isLocalhost); + console.debug('[NDK.ts] - Protocol:', window.location.protocol); + console.debug('[NDK.ts] - Is HTTP:', isHttp); + console.debug('[NDK.ts] - Is HTTPS:', isHttps); + + if (isLocalhost && isHttp) { + console.warn('[NDK.ts] ⚠️ Running on localhost with HTTP - WebSocket downgrade to ws:// is expected'); + console.warn('[NDK.ts] This is normal for development environments'); + } else if (isHttp) { + console.error('[NDK.ts] ❌ Running on HTTP - WebSocket connections will be insecure'); + console.error('[NDK.ts] Consider using HTTPS in production'); + } else if (isHttps) { + console.debug('[NDK.ts] ✓ Running on HTTPS - Secure WebSocket connections should work'); + } +} + +/** + * Checks WebSocket protocol support and logs diagnostic information + */ +export function checkWebSocketSupport(): void { + console.debug('[NDK.ts] WebSocket Support Diagnostics:'); + console.debug('[NDK.ts] - Protocol:', window.location.protocol); + console.debug('[NDK.ts] - Hostname:', window.location.hostname); + console.debug('[NDK.ts] - Port:', window.location.port); + console.debug('[NDK.ts] - User Agent:', navigator.userAgent); + + // Test if secure WebSocket is supported + try { + const testWs = new WebSocket('wss://echo.websocket.org'); + testWs.onopen = () => { + console.debug('[NDK.ts] ✓ Secure WebSocket (wss://) is supported'); + testWs.close(); + }; + testWs.onerror = () => { + console.warn('[NDK.ts] ✗ Secure WebSocket (wss://) may not be supported'); + }; + } catch (error) { + console.warn('[NDK.ts] ✗ WebSocket test failed:', error); + } +} + +/** + * Tests connection to a relay and returns connection status + * @param relayUrl The relay URL to test + * @param ndk The NDK instance + * @returns Promise that resolves to connection status + */ +export async function testRelayConnection(relayUrl: string, ndk: NDK): Promise<{ + connected: boolean; + requiresAuth: boolean; + error?: string; + actualUrl?: string; +}> { + return new Promise((resolve) => { + console.debug(`[NDK.ts] Testing connection to: ${relayUrl}`); + + // Ensure the URL is using wss:// protocol + const secureUrl = ensureSecureWebSocket(relayUrl); + + const relay = new NDKRelay(secureUrl, undefined, new NDK()); + let authRequired = false; + let connected = false; + let error: string | undefined; + let actualUrl: string | undefined; + + const timeout = setTimeout(() => { + relay.disconnect(); + resolve({ + connected: false, + requiresAuth: authRequired, + error: 'Connection timeout', + actualUrl + }); + }, 5000); + + relay.on('connect', () => { + console.debug(`[NDK.ts] Connected to ${secureUrl}`); + connected = true; + actualUrl = secureUrl; + clearTimeout(timeout); + relay.disconnect(); + resolve({ + connected: true, + requiresAuth: authRequired, + error, + actualUrl + }); + }); + + relay.on('notice', (message: string) => { + if (message.includes('auth-required')) { + authRequired = true; + console.debug(`[NDK.ts] ${secureUrl} requires authentication`); + } + }); + + relay.on('disconnect', () => { + if (!connected) { + error = 'Connection failed'; + console.error(`[NDK.ts] Failed to connect to ${secureUrl}`); + clearTimeout(timeout); + resolve({ + connected: false, + requiresAuth: authRequired, + error, + actualUrl + }); + } + }); + + // Log the actual WebSocket URL being used + console.debug(`[NDK.ts] Attempting connection to: ${secureUrl}`); + relay.connect(); + }); +} + /** * Gets the user's pubkey from local storage, if it exists. * @returns The user's pubkey, or null if there is no logged-in user. @@ -90,22 +321,57 @@ export function clearPersistedRelays(user: NDKUser): void { localStorage.removeItem(getRelayStorageKey(user, 'outbox')); } +/** + * Ensures a relay URL uses secure WebSocket protocol + * @param url The relay URL to secure + * @returns The URL with wss:// protocol + */ +function ensureSecureWebSocket(url: string): string { + // Replace ws:// with wss:// if present + const secureUrl = url.replace(/^ws:\/\//, 'wss://'); + + if (secureUrl !== url) { + console.warn(`[NDK.ts] Protocol downgrade detected: ${url} -> ${secureUrl}`); + } + + return secureUrl; +} + +/** + * Creates a relay with proper authentication handling + */ +function createRelayWithAuth(url: string, ndk: NDK): NDKRelay { + console.debug(`[NDK.ts] Creating relay with URL: ${url}`); + + // Ensure the URL is using wss:// protocol + const secureUrl = ensureSecureWebSocket(url); + + const relay = new NDKRelay(secureUrl, NDKRelayAuthPolicies.signIn({ ndk }), ndk); + + // Set up custom authentication handling only if user is signed in + if (ndk.signer && ndk.activeUser) { + const authPolicy = new CustomRelayAuthPolicy(ndk); + relay.on('connect', () => { + console.debug(`[NDK.ts] Relay connected: ${secureUrl}`); + authPolicy.authenticate(relay); + }); + } + + return relay; +} + export function getActiveRelays(ndk: NDK): NDKRelaySet { + // Use anonymous relays if user is not signed in + const isSignedIn = ndk.signer && ndk.activeUser; + const relays = isSignedIn ? standardRelays : anonymousRelays; + return get(feedType) === FeedType.UserRelays ? new NDKRelaySet( - new Set(get(inboxRelays).map(relay => new NDKRelay( - relay, - NDKRelayAuthPolicies.signIn({ ndk }), - ndk, - ))), + new Set(get(inboxRelays).map(relay => createRelayWithAuth(relay, ndk))), ndk ) : new NDKRelaySet( - new Set(standardRelays.map(relay => new NDKRelay( - relay, - NDKRelayAuthPolicies.signIn({ ndk }), - ndk, - ))), + new Set(relays.map(relay => createRelayWithAuth(relay, ndk))), ndk ); } @@ -121,17 +387,22 @@ export function initNdk(): NDK { ? getPersistedRelays(new NDKUser({ pubkey: startingPubkey })) : [null, null]; + // Ensure all relay URLs use secure WebSocket protocol + const secureRelayUrls = (startingInboxes != null + ? Array.from(startingInboxes.values()) + : anonymousRelays).map(ensureSecureWebSocket); + + console.debug('[NDK.ts] Initializing NDK with relay URLs:', secureRelayUrls); + const ndk = new NDK({ autoConnectUserRelays: true, enableOutboxModel: true, - explicitRelayUrls: startingInboxes != null - ? Array.from(startingInboxes.values()) - : standardRelays, + explicitRelayUrls: secureRelayUrls, }); - // TODO: Should we prompt the user to confirm authentication? + // Set up custom authentication policy ndk.relayAuthDefaultPolicy = NDKRelayAuthPolicies.signIn({ ndk }); - ndk.connect().then(() => console.debug("ndk connected")); + ndk.connect().then(() => console.debug("[NDK.ts] NDK connected")); return ndk; } @@ -150,7 +421,7 @@ export async function loginWithExtension(pubkey?: string): Promise { - const relay = new NDKRelay(url, NDKRelayAuthPolicies.signIn({ ndk }), ndk); + const relay = createRelayWithAuth(url, ndk); if (relayType.read) inboxRelays.add(relay); if (relayType.write) outboxRelays.add(relay); }); @@ -228,14 +500,14 @@ async function getUserPreferredRelays( relayList.tags.forEach(tag => { switch (tag[0]) { case 'r': - inboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); + inboxRelays.add(createRelayWithAuth(tag[1], ndk)); break; case 'w': - outboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); + outboxRelays.add(createRelayWithAuth(tag[1], ndk)); break; default: - inboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); - outboxRelays.add(new NDKRelay(tag[1], NDKRelayAuthPolicies.signIn({ ndk }), ndk)); + inboxRelays.add(createRelayWithAuth(tag[1], ndk)); + outboxRelays.add(createRelayWithAuth(tag[1], ndk)); break; } }); diff --git a/src/lib/utils/nostrUtils.ts b/src/lib/utils/nostrUtils.ts index 9e06d2e..ff7440b 100644 --- a/src/lib/utils/nostrUtils.ts +++ b/src/lib/utils/nostrUtils.ts @@ -4,7 +4,7 @@ import { ndkInstance } from '$lib/ndk'; import { npubCache } from './npubCache'; import NDK, { NDKEvent, NDKRelaySet, NDKUser } from "@nostr-dev-kit/ndk"; import type { NDKFilter, NDKKind } from "@nostr-dev-kit/ndk"; -import { standardRelays, fallbackRelays } from "$lib/consts"; +import { standardRelays, fallbackRelays, anonymousRelays } from "$lib/consts"; import { NDKRelaySet as NDKRelaySetFromNDK } from '@nostr-dev-kit/ndk'; import { sha256 } from '@noble/hashes/sha256'; import { schnorr } from '@noble/curves/secp256k1'; @@ -320,9 +320,13 @@ export async function fetchEventWithFallback( .map(r => r.url) : []; + // Determine which relays to use based on user authentication status + const isSignedIn = ndk.signer && ndk.activeUser; + const primaryRelays = isSignedIn ? standardRelays : anonymousRelays; + // Create three relay sets in priority order const relaySets = [ - NDKRelaySetFromNDK.fromRelayUrls(standardRelays, ndk), // 1. Standard relays + NDKRelaySetFromNDK.fromRelayUrls(primaryRelays, ndk), // 1. Primary relays (auth or anonymous) NDKRelaySetFromNDK.fromRelayUrls(userRelays, ndk), // 2. User relays (if logged in) NDKRelaySetFromNDK.fromRelayUrls(fallbackRelays, ndk) // 3. fallback relays (last resort) ]; @@ -347,7 +351,7 @@ export async function fetchEventWithFallback( // Try each relay set in order for (const [index, relaySet] of relaySets.entries()) { - const setName = index === 0 ? 'standard relays' : + const setName = index === 0 ? (isSignedIn ? 'standard relays' : 'anonymous relays') : index === 1 ? 'user relays' : 'fallback relays'; @@ -358,7 +362,7 @@ export async function fetchEventWithFallback( if (!found) { const timeoutSeconds = timeoutMs / 1000; const relayUrls = relaySets.map((set, i) => { - const setName = i === 0 ? 'standard relays' : + const setName = i === 0 ? (isSignedIn ? 'standard relays' : 'anonymous relays') : i === 1 ? 'user relays' : 'fallback relays'; const urls = Array.from(set.relays).map(r => r.url); diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index 5e31c26..cf37de1 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -1,6 +1,7 @@