# NIP-11 Relay Connection Implementation Plan ## Overview Implement NIP-11 relay information fetching in the web admin interface to replace hardcoded relay pubkey and provide proper relay connection flow. ## Current Issues 1. **Hardcoded Relay Pubkey**: `getRelayPubkey()` returns hardcoded value `'4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa'` 2. **Relay URL in Debug Section**: Currently in "DEBUG - TEST FETCH WITHOUT LOGIN" section (lines 336-385) 3. **No Relay Verification**: Users can attempt admin operations without verifying relay identity 4. **Missing NIP-11 Support**: No fetching of relay information document ## Implementation Plan ### 1. New Relay Connection Section (HTML Structure) Add after User Info section (around line 332): ```html

RELAY CONNECTION

NOT CONNECTED
``` ### 2. JavaScript Implementation #### Global State Variables Add to global state section (around line 535): ```javascript // Relay connection state let relayInfo = null; let isRelayConnected = false; let relayWebSocket = null; ``` #### NIP-11 Fetching Function Add new function: ```javascript // Fetch relay information using NIP-11 async function fetchRelayInfo(relayUrl) { try { console.log('=== FETCHING RELAY INFO VIA NIP-11 ==='); console.log('Relay URL:', relayUrl); // Convert WebSocket URL to HTTP URL for NIP-11 let httpUrl = relayUrl; if (relayUrl.startsWith('ws://')) { httpUrl = relayUrl.replace('ws://', 'http://'); } else if (relayUrl.startsWith('wss://')) { httpUrl = relayUrl.replace('wss://', 'https://'); } console.log('HTTP URL for NIP-11:', httpUrl); // Fetch relay information document const response = await fetch(httpUrl, { method: 'GET', headers: { 'Accept': 'application/nostr+json' }, // Add timeout signal: AbortSignal.timeout(10000) // 10 second timeout }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { throw new Error(`Invalid content type: ${contentType}. Expected application/json or application/nostr+json`); } const relayInfoData = await response.json(); console.log('Fetched relay info:', relayInfoData); // Validate required fields if (!relayInfoData.pubkey) { throw new Error('Relay information missing required pubkey field'); } // Validate pubkey format (64 hex characters) if (!/^[0-9a-fA-F]{64}$/.test(relayInfoData.pubkey)) { throw new Error(`Invalid relay pubkey format: ${relayInfoData.pubkey}`); } return relayInfoData; } catch (error) { console.error('Failed to fetch relay info:', error); throw error; } } ``` #### Relay Connection Function Add new function: ```javascript // Connect to relay and fetch information async function connectToRelay() { try { const relayUrlInput = document.getElementById('relay-url-input'); const connectBtn = document.getElementById('connect-relay-btn'); const disconnectBtn = document.getElementById('disconnect-relay-btn'); const statusDiv = document.getElementById('relay-connection-status'); const infoDisplay = document.getElementById('relay-info-display'); const url = relayUrlInput.value.trim(); if (!url) { throw new Error('Please enter a relay URL'); } // Update UI to show connecting state connectBtn.disabled = true; statusDiv.textContent = 'CONNECTING...'; statusDiv.className = 'status connected'; console.log('Connecting to relay:', url); // Fetch relay information via NIP-11 console.log('Fetching relay information...'); const fetchedRelayInfo = await fetchRelayInfo(url); // Test WebSocket connection console.log('Testing WebSocket connection...'); await testWebSocketConnection(url); // Store relay information relayInfo = fetchedRelayInfo; isRelayConnected = true; // Update UI with relay information displayRelayInfo(relayInfo); // Update connection status statusDiv.textContent = 'CONNECTED'; statusDiv.className = 'status connected'; // Update button states connectBtn.style.display = 'none'; disconnectBtn.style.display = 'inline-block'; relayUrlInput.disabled = true; // Show relay info infoDisplay.classList.remove('hidden'); console.log('Successfully connected to relay:', relayInfo.name || url); log(`Connected to relay: ${relayInfo.name || url}`, 'INFO'); } catch (error) { console.error('Failed to connect to relay:', error); // Reset UI state const connectBtn = document.getElementById('connect-relay-btn'); const statusDiv = document.getElementById('relay-connection-status'); connectBtn.disabled = false; statusDiv.textContent = `CONNECTION FAILED: ${error.message}`; statusDiv.className = 'status error'; // Clear any partial state relayInfo = null; isRelayConnected = false; log(`Failed to connect to relay: ${error.message}`, 'ERROR'); } } ``` #### WebSocket Connection Test Add new function: ```javascript // Test WebSocket connection to relay async function testWebSocketConnection(url) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { ws.close(); reject(new Error('WebSocket connection timeout')); }, 5000); const ws = new WebSocket(url); ws.onopen = () => { clearTimeout(timeout); console.log('WebSocket connection successful'); ws.close(); resolve(); }; ws.onerror = (error) => { clearTimeout(timeout); console.error('WebSocket connection failed:', error); reject(new Error('WebSocket connection failed')); }; ws.onclose = (event) => { if (event.code !== 1000) { clearTimeout(timeout); reject(new Error(`WebSocket closed with code ${event.code}: ${event.reason}`)); } }; }); } ``` #### Display Relay Information Add new function: ```javascript // Display relay information in the UI function displayRelayInfo(info) { document.getElementById('relay-name').textContent = info.name || 'Unknown'; document.getElementById('relay-description').textContent = info.description || 'No description'; document.getElementById('relay-pubkey-display').textContent = info.pubkey || 'Unknown'; document.getElementById('relay-software').textContent = info.software || 'Unknown'; document.getElementById('relay-version').textContent = info.version || 'Unknown'; document.getElementById('relay-contact').textContent = info.contact || 'No contact info'; // Format supported NIPs let nipsText = 'None specified'; if (info.supported_nips && Array.isArray(info.supported_nips) && info.supported_nips.length > 0) { nipsText = info.supported_nips.map(nip => `NIP-${nip.toString().padStart(2, '0')}`).join(', '); } document.getElementById('relay-nips').textContent = nipsText; } ``` #### Disconnect Function Add new function: ```javascript // Disconnect from relay function disconnectFromRelay() { console.log('Disconnecting from relay...'); // Clear relay state relayInfo = null; isRelayConnected = false; // Close any existing connections if (relayPool) { const url = document.getElementById('relay-url-input').value.trim(); if (url) { relayPool.close([url]); } relayPool = null; subscriptionId = null; } // Reset UI const connectBtn = document.getElementById('connect-relay-btn'); const disconnectBtn = document.getElementById('disconnect-relay-btn'); const statusDiv = document.getElementById('relay-connection-status'); const infoDisplay = document.getElementById('relay-info-display'); const relayUrlInput = document.getElementById('relay-url-input'); connectBtn.style.display = 'inline-block'; disconnectBtn.style.display = 'none'; connectBtn.disabled = false; relayUrlInput.disabled = false; statusDiv.textContent = 'NOT CONNECTED'; statusDiv.className = 'status disconnected'; infoDisplay.classList.add('hidden'); // Reset configuration status updateConfigStatus(false); log('Disconnected from relay', 'INFO'); } ``` #### Update getRelayPubkey Function Replace existing function (around line 3142): ```javascript // Helper function to get relay pubkey from connected relay info function getRelayPubkey() { if (relayInfo && relayInfo.pubkey) { return relayInfo.pubkey; } // Fallback to hardcoded value if no relay connected (for testing) console.warn('No relay connected, using fallback pubkey'); return '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa'; } ``` ### 3. Event Handlers Add event handlers in the DOMContentLoaded section: ```javascript // Relay connection event handlers const connectRelayBtn = document.getElementById('connect-relay-btn'); const disconnectRelayBtn = document.getElementById('disconnect-relay-btn'); if (connectRelayBtn) { connectRelayBtn.addEventListener('click', function(e) { e.preventDefault(); connectToRelay().catch(error => { console.error('Connect to relay failed:', error); }); }); } if (disconnectRelayBtn) { disconnectRelayBtn.addEventListener('click', function(e) { e.preventDefault(); disconnectFromRelay(); }); } ``` ### 4. Update Existing Functions #### Update fetchConfiguration Function Add relay connection check at the beginning: ```javascript async function fetchConfiguration() { try { console.log('=== FETCHING CONFIGURATION VIA ADMIN API ==='); // Check if relay is connected if (!isRelayConnected || !relayInfo) { throw new Error('Must be connected to relay first. Please connect to relay in the Relay Connection section.'); } // ... rest of existing function } catch (error) { // ... existing error handling } } ``` #### Update subscribeToConfiguration Function Add relay connection check: ```javascript async function subscribeToConfiguration() { try { console.log('=== STARTING SIMPLEPOOL CONFIGURATION SUBSCRIPTION ==='); if (!isRelayConnected || !relayInfo) { console.error('Must be connected to relay first'); return false; } // Use the relay URL from the connection section instead of the debug section const url = document.getElementById('relay-url-input').value.trim(); // ... rest of existing function } catch (error) { // ... existing error handling } } ``` ### 5. Update UI Flow #### Modify showMainInterface Function Update to show relay connection requirement: ```javascript function showMainInterface() { loginSection.classList.add('hidden'); mainInterface.classList.remove('hidden'); userPubkeyDisplay.textContent = userPubkey; // Show message about relay connection requirement if (!isRelayConnected) { log('Please connect to a relay to access admin functions', 'INFO'); } } ``` ### 6. Remove/Update Debug Section #### Option 1: Remove Debug Section Entirely Remove the "DEBUG - TEST FETCH WITHOUT LOGIN" section (lines 335-385) since relay URL is now in the proper connection section. #### Option 2: Keep Debug Section for Testing Update the debug section to use the connected relay URL and add a note that it's for testing purposes. ### 7. Error Handling Add comprehensive error handling for: - Network timeouts - Invalid relay URLs - Missing NIP-11 support - Invalid relay pubkey format - WebSocket connection failures - CORS issues ### 8. Security Considerations - Validate relay pubkey format (64 hex characters) - Verify relay identity before admin operations - Handle CORS properly for NIP-11 requests - Sanitize relay information display - Warn users about connecting to untrusted relays ## Testing Plan 1. **NIP-11 Fetching**: Test with various relay URLs (localhost, remote relays) 2. **Error Handling**: Test with invalid URLs, non-Nostr servers, network failures 3. **WebSocket Connection**: Verify WebSocket connectivity after NIP-11 fetch 4. **Admin API Integration**: Ensure admin commands use correct relay pubkey 5. **UI Flow**: Test complete user journey from login → relay connection → admin operations ## Benefits 1. **Proper Relay Identification**: Uses actual relay pubkey instead of hardcoded value 2. **Better UX**: Clear connection flow and relay information display 3. **Protocol Compliance**: Implements NIP-11 standard for relay discovery 4. **Security**: Verifies relay identity before admin operations 5. **Flexibility**: Works with any NIP-11 compliant relay ## Migration Notes - Existing users will need to connect to relay after this update - Debug section can be kept for development/testing purposes - All admin functions will require relay connection - Relay pubkey will be dynamically fetched instead of hardcoded