Files
c-relay/nip11_relay_connection_implementation_plan.md
2025-09-30 05:32:23 -04:00

15 KiB

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):

<!-- Relay Connection Section -->
<div class="section">
    <h2>RELAY CONNECTION</h2>
    <div class="input-group">
        <label for="relay-url-input">Relay URL:</label>
        <input type="text" id="relay-url-input" value="ws://localhost:8888" placeholder="ws://localhost:8888 or wss://relay.example.com">
    </div>
    <div class="inline-buttons">
        <button type="button" id="connect-relay-btn">CONNECT TO RELAY</button>
        <button type="button" id="disconnect-relay-btn" style="display: none;">DISCONNECT</button>
    </div>
    <div class="status disconnected" id="relay-connection-status">NOT CONNECTED</div>
    
    <!-- Relay Information Display -->
    <div id="relay-info-display" class="hidden">
        <h3>Relay Information</h3>
        <div class="user-info">
            <div><strong>Name:</strong> <span id="relay-name">-</span></div>
            <div><strong>Description:</strong> <span id="relay-description">-</span></div>
            <div><strong>Public Key:</strong>
                <div class="user-pubkey" id="relay-pubkey-display">-</div>
            </div>
            <div><strong>Software:</strong> <span id="relay-software">-</span></div>
            <div><strong>Version:</strong> <span id="relay-version">-</span></div>
            <div><strong>Contact:</strong> <span id="relay-contact">-</span></div>
            <div><strong>Supported NIPs:</strong> <span id="relay-nips">-</span></div>
        </div>
    </div>
</div>

2. JavaScript Implementation

Global State Variables

Add to global state section (around line 535):

// Relay connection state
let relayInfo = null;
let isRelayConnected = false;
let relayWebSocket = null;

NIP-11 Fetching Function

Add new function:

// 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:

// 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:

// 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:

// 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:

// 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):

// 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:

// 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:

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:

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:

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