// Global error handler to prevent page refreshes window.addEventListener('error', function (e) { console.error('Global error caught:', e.error); console.error('Error message:', e.message); console.error('Error filename:', e.filename); console.error('Error line:', e.lineno); e.preventDefault(); // Prevent default browser error handling return true; // Prevent page refresh }); window.addEventListener('unhandledrejection', function (e) { console.error('Unhandled promise rejection:', e.reason); e.preventDefault(); // Prevent default browser error handling return true; // Prevent page refresh }); // Global state let nlLite = null; let userPubkey = null; let isLoggedIn = false; let currentConfig = null; // Global subscription state let relayPool = null; let subscriptionId = null; // Relay connection state let relayInfo = null; let isRelayConnected = false; let relayPubkey = null; // DOM elements const loginSection = document.getElementById('login-section'); // const mainInterface = document.getElementById('main-interface'); const persistentUserName = document.getElementById('persistent-user-name'); const persistentUserPubkey = document.getElementById('persistent-user-pubkey'); const persistentUserAbout = document.getElementById('persistent-user-about'); const persistentUserDetails = document.getElementById('persistent-user-details'); const fetchConfigBtn = document.getElementById('fetch-config-btn'); // Relay connection elements const relayConnectionUrl = document.getElementById('relay-connection-url'); const relayPubkeyManual = document.getElementById('relay-pubkey-manual'); const relayConnectionStatus = document.getElementById('relay-connection-status'); const connectRelayBtn = document.getElementById('connect-relay-btn'); const disconnectRelayBtn = document.getElementById('disconnect-relay-btn'); const restartRelayBtn = document.getElementById('restart-relay-btn'); const configDisplay = document.getElementById('config-display'); const configTableBody = document.getElementById('config-table-body'); // NIP-17 DM elements const dmOutbox = document.getElementById('dm-outbox'); const dmInbox = document.getElementById('dm-inbox'); const sendDmBtn = document.getElementById('send-dm-btn'); // Utility functions function log(message, type = 'INFO') { const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; const logMessage = `${timestamp} [${type}]: ${message}`; // Always log to browser console so we don't lose logs on refresh console.log(logMessage); // UI logging removed - using console only } // Utility functions function log(message, type = 'INFO') { const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; const logMessage = `${timestamp} [${type}]: ${message}`; // Always log to browser console so we don't lose logs on refresh console.log(logMessage); // UI logging removed - using console only } // NIP-59 helper: randomize created_at to thwart time-analysis (past 2 days) function randomNow() { const TWO_DAYS = 2 * 24 * 60 * 60; // 172800 seconds const now = Math.round(Date.now() / 1000); return Math.round(now - Math.random() * TWO_DAYS); } // Safe JSON parse with error handling function safeJsonParse(jsonString) { try { return JSON.parse(jsonString); } catch (error) { console.error('JSON parse error:', error); return null; } } // ================================ // NIP-11 RELAY CONNECTION FUNCTIONS // ================================ // Convert WebSocket URL to HTTP URL for NIP-11 function wsToHttpUrl(wsUrl) { if (wsUrl.startsWith('ws://')) { return wsUrl.replace('ws://', 'http://'); } else if (wsUrl.startsWith('wss://')) { return wsUrl.replace('wss://', 'https://'); } return wsUrl; } // Fetch relay information using NIP-11 async function fetchRelayInfo(relayUrl) { try { log(`Fetching NIP-11 relay info from: ${relayUrl}`, 'INFO'); // Convert WebSocket URL to HTTP URL const httpUrl = wsToHttpUrl(relayUrl); // Make HTTP request with NIP-11 headers const response = await fetch(httpUrl, { method: 'GET', headers: { 'Accept': 'application/nostr+json', 'User-Agent': 'C-Relay-Admin-API/1.0' }, 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/nostr+json')) { throw new Error(`Invalid content type: ${contentType}. Expected application/nostr+json`); } const relayInfo = await response.json(); // Log if relay info is empty (not configured yet) but don't throw error if (!relayInfo || Object.keys(relayInfo).length === 0) { log('Relay returned empty NIP-11 info - relay not configured yet, will use manual pubkey if provided', 'INFO'); // Return empty object - this is valid, caller will handle manual pubkey fallback return {}; } // Validate pubkey if present if (relayInfo.pubkey && !/^[0-9a-fA-F]{64}$/.test(relayInfo.pubkey)) { throw new Error(`Invalid relay pubkey format: ${relayInfo.pubkey}`); } log(`Successfully fetched relay info. Pubkey: ${relayInfo.pubkey ? relayInfo.pubkey.substring(0, 16) + '...' : 'not set'}`, 'INFO'); return relayInfo; } catch (error) { log(`Failed to fetch relay info: ${error.message}`, 'ERROR'); throw error; } } // Test WebSocket connection to relay async function testWebSocketConnection(wsUrl) { return new Promise((resolve, reject) => { try { log(`Testing WebSocket connection to: ${wsUrl}`, 'INFO'); const ws = new WebSocket(wsUrl); const timeout = setTimeout(() => { ws.close(); reject(new Error('WebSocket connection timeout (10s)')); }, 10000); ws.onopen = () => { clearTimeout(timeout); log('WebSocket connection successful', 'INFO'); ws.close(); resolve(true); }; ws.onerror = (error) => { clearTimeout(timeout); log(`WebSocket connection failed: ${error.message || 'Unknown error'}`, 'ERROR'); reject(new Error('WebSocket connection failed')); }; ws.onclose = (event) => { if (event.code !== 1000) { // 1000 = normal closure clearTimeout(timeout); reject(new Error(`WebSocket closed unexpectedly: ${event.code} ${event.reason}`)); } }; } catch (error) { log(`WebSocket test error: ${error.message}`, 'ERROR'); reject(error); } }); } // Connect to relay (NIP-11 + WebSocket test) async function connectToRelay() { try { const url = relayConnectionUrl.value.trim(); if (!url) { throw new Error('Please enter a relay URL'); } // Update UI to show connecting state updateRelayConnectionStatus('connecting'); connectRelayBtn.disabled = true; log(`Connecting to relay: ${url}`, 'INFO'); let fetchedRelayInfo; try { // Step 1: Try to fetch NIP-11 relay information fetchedRelayInfo = await fetchRelayInfo(url); // Check if NIP-11 response includes a pubkey if (fetchedRelayInfo.pubkey) { // NIP-11 provided pubkey - populate the manual input field log(`NIP-11 provided relay pubkey: ${fetchedRelayInfo.pubkey.substring(0, 16)}...`, 'INFO'); relayPubkeyManual.value = fetchedRelayInfo.pubkey; } else { // NIP-11 response missing pubkey, check for manual input log('NIP-11 response missing pubkey, checking for manual input...', 'INFO'); const manualPubkey = relayPubkeyManual.value.trim(); if (!manualPubkey) { throw new Error('Relay NIP-11 response does not include a pubkey. Please enter the relay pubkey manually (shown during relay startup).'); } if (!/^[0-9a-fA-F]{64}$/.test(manualPubkey)) { throw new Error('Manual relay pubkey must be exactly 64 hexadecimal characters'); } log(`Using manual relay pubkey: ${manualPubkey.substring(0, 16)}...`, 'INFO'); // Add manual pubkey to the fetched relay info fetchedRelayInfo.pubkey = manualPubkey; // If relay info was completely empty, create minimal info if (Object.keys(fetchedRelayInfo).length === 1) { fetchedRelayInfo = { name: 'C-Relay (Manual Config)', description: 'C-Relay instance - pubkey provided manually', pubkey: manualPubkey, contact: 'admin@manual.config.relay', supported_nips: [1, 9, 11, 13, 15, 20, 33, 40, 42], software: 'https://github.com/0xtrr/c-relay', version: '1.0.0' }; } } } catch (nip11Error) { // If NIP-11 completely fails (network error, etc.), require manual pubkey const manualPubkey = relayPubkeyManual.value.trim(); if (!manualPubkey) { throw new Error(`NIP-11 fetch failed: ${nip11Error.message}. Please enter the relay pubkey manually if the relay hasn't been configured yet.`); } if (!/^[0-9a-fA-F]{64}$/.test(manualPubkey)) { throw new Error('Manual relay pubkey must be exactly 64 hexadecimal characters'); } log(`NIP-11 failed, using manual relay pubkey: ${manualPubkey.substring(0, 16)}...`, 'INFO'); // Create minimal relay info with manual pubkey fetchedRelayInfo = { name: 'C-Relay (Manual Config)', description: 'C-Relay instance - pubkey provided manually', pubkey: manualPubkey, contact: 'admin@manual.config.relay', supported_nips: [1, 9, 11, 13, 15, 20, 33, 40, 42], software: 'https://github.com/0xtrr/c-relay', version: '1.0.0' }; } // Step 2: Test WebSocket connection await testWebSocketConnection(url); // Step 3: Update global state relayInfo = fetchedRelayInfo; relayPubkey = fetchedRelayInfo.pubkey; isRelayConnected = true; // Step 4: Update UI updateRelayConnectionStatus('connected'); updateAdminSectionsVisibility(); // Step 5: Relay URL updated // Step 6: Automatically load configuration and auth rules log('Relay connected successfully. Auto-loading configuration and auth rules...', 'INFO'); // Auto-fetch configuration setTimeout(() => { fetchConfiguration().catch(error => { log('Auto-fetch configuration failed: ' + error.message, 'ERROR'); }); }, 500); // Auto-fetch auth rules setTimeout(() => { loadAuthRules().catch(error => { log('Auto-fetch auth rules failed: ' + error.message, 'ERROR'); }); }, 1000); // Auto-fetch database statistics setTimeout(() => { sendStatsQuery().catch(error => { log('Auto-fetch statistics failed: ' + error.message, 'ERROR'); }); }, 1500); log(`Successfully connected to relay: ${relayInfo.name || 'Unknown'}`, 'INFO'); } catch (error) { log(`Failed to connect to relay: ${error.message}`, 'ERROR'); updateRelayConnectionStatus('error'); // Reset state on failure relayInfo = null; relayPubkey = null; isRelayConnected = false; } finally { connectRelayBtn.disabled = false; } } // Disconnect from relay function disconnectFromRelay() { try { log('Disconnecting from relay...', 'INFO'); // Clean up relay pool if exists if (relayPool) { const url = relayConnectionUrl.value.trim(); if (url) { relayPool.close([url]); } relayPool = null; subscriptionId = null; } // Reset state relayInfo = null; relayPubkey = null; isRelayConnected = false; // Update UI updateRelayConnectionStatus('disconnected'); hideRelayInfo(); updateAdminSectionsVisibility(); log('Disconnected from relay', 'INFO'); } catch (error) { log(`Error during relay disconnection: ${error.message}`, 'ERROR'); } } // Update relay connection status UI function updateRelayConnectionStatus(status) { if (!relayConnectionStatus) return; switch (status) { case 'connecting': relayConnectionStatus.textContent = 'CONNECTING...'; relayConnectionStatus.className = 'status connected'; connectRelayBtn.disabled = true; disconnectRelayBtn.disabled = true; restartRelayBtn.disabled = true; break; case 'connected': relayConnectionStatus.textContent = 'CONNECTED'; relayConnectionStatus.className = 'status connected'; connectRelayBtn.disabled = true; disconnectRelayBtn.disabled = false; restartRelayBtn.disabled = false; break; case 'disconnected': relayConnectionStatus.textContent = 'NOT CONNECTED'; relayConnectionStatus.className = 'status disconnected'; connectRelayBtn.disabled = false; disconnectRelayBtn.disabled = true; restartRelayBtn.disabled = true; break; case 'error': relayConnectionStatus.textContent = 'CONNECTION FAILED'; relayConnectionStatus.className = 'status error'; connectRelayBtn.disabled = false; disconnectRelayBtn.disabled = true; restartRelayBtn.disabled = true; break; } } // Hide relay information display (placeholder for removed functionality) function hideRelayInfo() { // Relay info display functionality has been removed console.log('Relay info display functionality has been removed'); } // Check for existing authentication state with multiple API methods and retry logic async function checkExistingAuthWithRetries() { console.log('Starting authentication state detection with retry logic...'); const maxAttempts = 10; const delay = 500; // ms between attempts for (let attempt = 1; attempt <= maxAttempts; attempt++) { console.log(`Authentication detection attempt ${attempt}/${maxAttempts}`); try { // Method 1: Try window.NOSTR_LOGIN_LITE.getAuthState() if (window.NOSTR_LOGIN_LITE && typeof window.NOSTR_LOGIN_LITE.getAuthState === 'function') { console.log('Trying window.NOSTR_LOGIN_LITE.getAuthState()...'); const authState = window.NOSTR_LOGIN_LITE.getAuthState(); if (authState && authState.pubkey) { console.log('✅ Auth state found via NOSTR_LOGIN_LITE.getAuthState():', authState.pubkey); await restoreAuthenticationState(authState.pubkey); return true; } } // Method 2: Try nlLite.getPublicKey() if (nlLite && typeof nlLite.getPublicKey === 'function') { console.log('Trying nlLite.getPublicKey()...'); const pubkey = await nlLite.getPublicKey(); if (pubkey && pubkey.length === 64) { console.log('✅ Pubkey found via nlLite.getPublicKey():', pubkey); await restoreAuthenticationState(pubkey); return true; } } // Method 3: Try window.nostr.getPublicKey() (NIP-07) if (window.nostr && typeof window.nostr.getPublicKey === 'function') { console.log('Trying window.nostr.getPublicKey()...'); const pubkey = await window.nostr.getPublicKey(); if (pubkey && pubkey.length === 64) { console.log('✅ Pubkey found via window.nostr.getPublicKey():', pubkey); await restoreAuthenticationState(pubkey); return true; } } // Method 4: Check localStorage directly for NOSTR_LOGIN_LITE data const localStorageData = localStorage.getItem('NOSTR_LOGIN_LITE_DATA'); if (localStorageData) { try { const parsedData = JSON.parse(localStorageData); if (parsedData.pubkey) { console.log('✅ Pubkey found in localStorage:', parsedData.pubkey); await restoreAuthenticationState(parsedData.pubkey); return true; } } catch (parseError) { console.log('Failed to parse localStorage data:', parseError.message); } } console.log(`❌ Attempt ${attempt}: No authentication found via any method`); // Wait before next attempt (except for last attempt) if (attempt < maxAttempts) { await new Promise(resolve => setTimeout(resolve, delay)); } } catch (error) { console.log(`❌ Attempt ${attempt} failed:`, error.message); if (attempt < maxAttempts) { await new Promise(resolve => setTimeout(resolve, delay)); } } } console.log('🔍 Authentication detection completed - no existing auth found after all attempts'); return false; } // Helper function to restore authentication state async function restoreAuthenticationState(pubkey) { console.log('🔄 Restoring authentication state for pubkey:', pubkey); userPubkey = pubkey; isLoggedIn = true; // Show main interface showMainInterface(); loadUserProfile(); updateLoginLogoutButton(); // Note: Configuration fetching now requires explicit relay connection // User must connect to relay manually after login console.log('✅ Authentication state restored - connect to relay to fetch configuration'); console.log('✅ Authentication state restored successfully'); } // Legacy function for backward compatibility async function checkExistingAuth() { return await checkExistingAuthWithRetries(); } // Initialize NOSTR_LOGIN_LITE async function initializeApp() { try { await window.NOSTR_LOGIN_LITE.init({ theme: 'default', methods: { extension: true, local: true, seedphrase: true, readonly: true, connect: true, remote: true, otp: false }, floatingTab: { enabled: false, // hPosition: 1, // 0.0-1.0 or '95%' from left // vPosition: 0, // 0.0-1.0 or '50%' from top // appearance: { // style: 'square', // 'pill', 'square', 'circle', 'minimal' // // icon: '[LOGIN]', // Now uses text-based icons like [LOGIN], [KEY], [NET] // text: 'Login' // }, // behavior: { // hideWhenAuthenticated: false, // showUserInfo: true, // autoSlide: true // }, // animation: { // slideDirection: 'auto' // 'auto', 'left', 'right', 'up', 'down' // } } }); nlLite = window.NOSTR_LOGIN_LITE; console.log('Nostr login system initialized'); // Check for existing authentication state after initialization const wasAlreadyLoggedIn = await checkExistingAuth(); if (wasAlreadyLoggedIn) { console.log('User was already logged in, main interface restored'); } else { console.log('No existing authentication found, showing login interface'); } // Listen for authentication events window.addEventListener('nlMethodSelected', handleAuthEvent); window.addEventListener('nlLogout', handleLogoutEvent); } catch (error) { console.log('Failed to initialize Nostr login: ' + error.message); } } // Handle authentication events function handleAuthEvent(event) { const { pubkey, method, error } = event.detail; if (method && pubkey) { userPubkey = pubkey; isLoggedIn = true; console.log(`Login successful! Method: ${method}`); console.log(`Public key: ${pubkey}`); showMainInterface(); loadUserProfile(); updateLoginLogoutButton(); // Note: Configuration fetching now requires explicit relay connection // User must connect to relay manually after login console.log('Login successful. Connect to relay to access admin functions.'); } else if (error) { console.log(`Authentication error: ${error}`); } } // Handle logout events function handleLogoutEvent() { console.log('Logout event received'); userPubkey = null; isLoggedIn = false; currentConfig = null; // Clean up relay connection disconnectFromRelay(); // Reset UI // mainInterface.classList.add('hidden'); loginSection.classList.remove('hidden'); updateConfigStatus(false); updateLoginLogoutButton(); updateAdminSectionsVisibility(); console.log('Logout event handled successfully'); } // Update visibility of admin sections based on login and relay connection status function updateAdminSectionsVisibility() { const divConfig = document.getElementById('div_config'); const authRulesSection = document.getElementById('authRulesSection'); const nip17DMSection = document.getElementById('nip17DMSection'); const shouldShow = isLoggedIn && isRelayConnected; if (divConfig) divConfig.style.display = shouldShow ? 'block' : 'none'; if (authRulesSection) authRulesSection.style.display = shouldShow ? 'block' : 'none'; if (nip17DMSection) nip17DMSection.style.display = shouldShow ? 'block' : 'none'; } // Show main interface after login function showMainInterface() { loginSection.classList.add('hidden'); // mainInterface.classList.remove('hidden'); updateLoginLogoutButton(); updateAdminSectionsVisibility(); } // Load user profile using nostr-tools pool async function loadUserProfile() { if (!userPubkey) return; console.log('Loading user profile...'); persistentUserName.textContent = 'Loading...'; persistentUserAbout.textContent = 'Loading...'; try { // Create a SimplePool instance for profile loading const profilePool = new window.NostrTools.SimplePool(); const relays = ['wss://relay.laantungir.net']; // Get profile event (kind 0) for the user const events = await profilePool.querySync(relays, { kinds: [0], authors: [userPubkey], limit: 1 }); if (events.length > 0) { console.log('Profile event found:', events[0]); const profile = JSON.parse(events[0].content); console.log('Parsed profile:', profile); displayProfile(profile); } else { console.log('No profile events found for pubkey:', userPubkey); persistentUserName.textContent = 'Anonymous User'; persistentUserAbout.textContent = 'No profile found'; // Still show the pubkey since we have it persistentUserPubkey.textContent = userPubkey; } // Close the profile pool profilePool.close(relays); } catch (error) { console.log('Profile loading failed: ' + error.message); persistentUserName.textContent = 'Error loading profile'; persistentUserAbout.textContent = error.message; // Still show the pubkey since we have it persistentUserPubkey.textContent = userPubkey; } } // Display profile data function displayProfile(profile) { const name = profile.name || profile.display_name || profile.displayName || 'Anonymous User'; const about = profile.about || 'No description provided'; // Update persistent user details persistentUserName.textContent = name; persistentUserPubkey.textContent = userPubkey; persistentUserAbout.textContent = about; console.log(`Profile loaded for: ${name} with pubkey: ${userPubkey}`); } // Logout function async function logout() { console.log('Logging out...'); try { // Clean up relay connection disconnectFromRelay(); // Clean up configuration pool if (relayPool) { console.log('Closing configuration pool...'); const url = relayConnectionUrl.value.trim(); if (url) { relayPool.close([url]); } relayPool = null; subscriptionId = null; } await nlLite.logout(); userPubkey = null; isLoggedIn = false; currentConfig = null; // Reset UI - keep persistent auth container visible // mainInterface.classList.add('hidden'); loginSection.classList.remove('hidden'); updateConfigStatus(false); updateLoginLogoutButton(); console.log('Logged out successfully'); } catch (error) { console.log('Logout failed: ' + error.message); } } function updateConfigStatus(loaded) { if (loaded) { configDisplay.classList.remove('hidden'); } else { configDisplay.classList.add('hidden'); } } // Generate random subscription ID (avoiding colons which are rejected by relay) function generateSubId() { // Use only alphanumeric characters, underscores, and hyphens const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'; let result = ''; for (let i = 0; i < 12; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } // Configuration subscription using nostr-tools SimplePool async function subscribeToConfiguration() { try { console.log('=== STARTING SIMPLEPOOL CONFIGURATION SUBSCRIPTION ==='); if (!isLoggedIn) { console.log('WARNING: Not logged in, but proceeding with subscription test'); } const url = relayConnectionUrl.value.trim(); if (!url) { console.error('Please enter a relay URL'); return false; } console.log(`Connecting to relay via SimplePool: ${url}`); // Clean up existing pool if (relayPool) { console.log('Closing existing pool connection'); relayPool.close([url]); relayPool = null; subscriptionId = null; } // Create new SimplePool instance relayPool = new window.NostrTools.SimplePool(); subscriptionId = generateSubId(); console.log(`Generated subscription ID: ${subscriptionId}`); console.log(`User pubkey ${userPubkey}`) // Subscribe to kind 23457 events (admin response events), kind 4 (NIP-04 DMs), and kind 1059 (NIP-17 GiftWrap) const subscription = relayPool.subscribeMany([url], [{ since: Math.floor(Date.now() / 1000) - 5, // Look back 5 seconds to avoid race condition kinds: [23457], authors: [getRelayPubkey()], // Only listen to responses from the relay "#p": [userPubkey], // Only responses directed to this user limit: 50 }, { since: Math.floor(Date.now() / 1000), kinds: [4], // NIP-04 Direct Messages authors: [getRelayPubkey()], // Only listen to DMs from the relay "#p": [userPubkey], // Only DMs directed to this user limit: 50 }, { kinds: [1059], // NIP-17 GiftWrap events "#p": [userPubkey], // Only GiftWrap events addressed to this user limit: 50 }], { async onevent(event) { console.log('=== EVENT RECEIVED VIA SIMPLEPOOL ==='); console.log('Event data:', event); console.log('Event kind:', event.kind); console.log('Event tags:', event.tags); console.log('Event pubkey:', event.pubkey); console.log('=== END EVENT ==='); // Handle NIP-04 DMs if (event.kind === 4) { console.log('=== NIP-04 DM RECEIVED ==='); try { // Decrypt the DM content const decryptedContent = await window.nostr.nip04.decrypt(event.pubkey, event.content); log(`Received NIP-04 DM from relay: ${decryptedContent.substring(0, 50)}...`, 'INFO'); // Add to inbox const timestamp = new Date(event.created_at * 1000).toLocaleString(); addMessageToInbox('received', decryptedContent, timestamp); // Log for testing if (typeof logTestEvent === 'function') { logTestEvent('RECV', `NIP-04 DM: ${decryptedContent}`, 'DM'); } } catch (decryptError) { log(`Failed to decrypt NIP-04 DM: ${decryptError.message}`, 'ERROR'); if (typeof logTestEvent === 'function') { logTestEvent('ERROR', `Failed to decrypt DM: ${decryptError.message}`, 'DM'); } } return; } // Handle NIP-17 GiftWrap DMs if (event.kind === 1059) { console.log('=== NIP-17 GIFTWRAP RECEIVED ==='); try { // Step 1: Unwrap gift wrap to get seal const sealJson = await window.nostr.nip44.decrypt(event.pubkey, event.content); const seal = safeJsonParse(sealJson); if (!seal || seal.kind !== 13) { throw new Error('Unwrapped content is not a valid seal (kind 13)'); } // Step 2: Unseal to get rumor const rumorJson = await window.nostr.nip44.decrypt(seal.pubkey, seal.content); const rumor = safeJsonParse(rumorJson); if (!rumor || rumor.kind !== 14) { throw new Error('Unsealed content is not a valid rumor (kind 14)'); } log(`Received NIP-17 DM from relay: ${rumor.content.substring(0, 50)}...`, 'INFO'); // Add to inbox const timestamp = new Date(event.created_at * 1000).toLocaleString(); addMessageToInbox('received', rumor.content, timestamp); // Log for testing if (typeof logTestEvent === 'function') { logTestEvent('RECV', `NIP-17 DM: ${rumor.content}`, 'DM'); } } catch (unwrapError) { log(`Failed to unwrap NIP-17 DM: ${unwrapError.message}`, 'ERROR'); if (typeof logTestEvent === 'function') { logTestEvent('ERROR', `Failed to unwrap DM: ${unwrapError.message}`, 'DM'); } } return; } // Handle admin response events (kind 23457) if (event.kind === 23457) { // Log all received messages for testing if (typeof logTestEvent === 'function') { logTestEvent('RECV', `Admin response event: ${JSON.stringify(event)}`, 'EVENT'); } // Process admin response event processAdminResponse(event); } }, oneose() { console.log('EOSE received - End of stored events'); console.log('Current config after EOSE:', currentConfig); if (!currentConfig) { console.log('No configuration events were received'); } }, onclose(reason) { console.log('Subscription closed:', reason); updateConfigStatus(false); } }); // Store subscription for cleanup relayPool.currentSubscription = subscription; console.log('SimplePool subscription established'); return true; } catch (error) { console.error('Configuration subscription failed:', error.message); console.error('Configuration subscription failed:', error); console.error('Error stack:', error.stack); return false; } } // Process admin response events (kind 23457) async function processAdminResponse(event) { try { console.log('=== PROCESSING ADMIN RESPONSE ==='); console.log('Response event:', event); // Verify this is a kind 23457 admin response event if (event.kind !== 23457) { console.log('Ignoring non-admin response event, kind:', event.kind); return; } // Verify the event is from the relay const expectedRelayPubkey = getRelayPubkey(); if (event.pubkey !== expectedRelayPubkey) { console.log('Ignoring response from unknown pubkey:', event.pubkey); return; } // Decrypt the NIP-44 encrypted content const decryptedContent = await decryptFromRelay(event.content); if (!decryptedContent) { throw new Error('Failed to decrypt admin response content'); } console.log('Decrypted admin response:', decryptedContent); // Parse the decrypted JSON response const responseData = JSON.parse(decryptedContent); console.log('Parsed response data:', responseData); // Log the response for testing if (typeof logTestEvent === 'function') { logTestEvent('RECV', `Decrypted response: ${JSON.stringify(responseData)}`, 'RESPONSE'); } // Handle different types of admin responses handleAdminResponseData(responseData); } catch (error) { console.error('Error processing admin response:', error); if (typeof logTestEvent === 'function') { logTestEvent('ERROR', `Failed to process admin response: ${error.message}`, 'ERROR'); } } } // Handle different types of admin response data function handleAdminResponseData(responseData) { try { console.log('=== HANDLING ADMIN RESPONSE DATA ==='); console.log('Response data:', responseData); console.log('Response query_type:', responseData.query_type); // Handle auth query responses - updated to match backend response types if (responseData.query_type && (responseData.query_type.includes('auth_rules') || responseData.query_type.includes('auth'))) { console.log('Routing to auth query handler'); handleAuthQueryResponse(responseData); return; } // Handle config update responses specifically if (responseData.query_type === 'config_update') { console.log('Routing to config update handler'); handleConfigUpdateResponse(responseData); return; } // Handle config query responses - updated to match backend response types if (responseData.query_type && (responseData.query_type.includes('config') || responseData.query_type.startsWith('config_'))) { console.log('Routing to config query handler'); handleConfigQueryResponse(responseData); return; } // Handle system command responses if (responseData.command) { console.log('Routing to system command handler'); handleSystemCommandResponse(responseData); return; } // Handle auth rule modification responses if (responseData.operation || responseData.rules_processed !== undefined) { console.log('Routing to auth rule modification handler'); handleAuthRuleResponse(responseData); return; } // Handle stats query responses if (responseData.query_type === 'stats_query') { console.log('Routing to stats query handler'); handleStatsQueryResponse(responseData); return; } // Generic response handling console.log('Using generic response handler'); if (typeof logTestEvent === 'function') { logTestEvent('RECV', `Generic admin response: ${JSON.stringify(responseData)}`, 'RESPONSE'); } } catch (error) { console.error('Error handling admin response data:', error); if (typeof logTestEvent === 'function') { logTestEvent('ERROR', `Failed to handle response data: ${error.message}`, 'ERROR'); } } } // Handle config query responses function handleConfigQueryResponse(responseData) { console.log('=== CONFIG QUERY RESPONSE ==='); console.log('Query type:', responseData.query_type); console.log('Total results:', responseData.total_results); console.log('Data:', responseData.data); // Convert the config response data to the format expected by displayConfiguration if (responseData.data && responseData.data.length > 0) { console.log('Converting config response to display format...'); // Create a synthetic event structure for displayConfiguration const syntheticEvent = { id: 'config_response_' + Date.now(), pubkey: getRelayPubkey(), created_at: Math.floor(Date.now() / 1000), kind: 'config_response', content: 'Configuration from admin API', tags: [] }; // Convert config data to tags format responseData.data.forEach(config => { const key = config.key || config.config_key; const value = config.value || config.config_value; if (key && value !== undefined) { syntheticEvent.tags.push([key, value]); } }); console.log('Synthetic event created:', syntheticEvent); console.log('Calling displayConfiguration with synthetic event...'); // Display the configuration using the original display function displayConfiguration(syntheticEvent); log(`Configuration loaded: ${responseData.total_results} parameters`, 'INFO'); } else { console.log('No configuration data received'); updateConfigStatus(false); } // Also log to test interface for debugging if (typeof logTestEvent === 'function') { logTestEvent('RECV', `Config query response: ${responseData.query_type}, ${responseData.total_results} results`, 'CONFIG_QUERY'); if (responseData.data && responseData.data.length > 0) { logTestEvent('RECV', '=== CONFIGURATION VALUES ===', 'CONFIG'); responseData.data.forEach((config, index) => { const key = config.key || config.config_key || `config_${index}`; const value = config.value || config.config_value || 'undefined'; const category = config.category || 'general'; const dataType = config.data_type || 'string'; logTestEvent('RECV', `${key}: ${value} (${dataType}, ${category})`, 'CONFIG'); }); logTestEvent('RECV', '=== END CONFIGURATION VALUES ===', 'CONFIG'); } else { logTestEvent('RECV', 'No configuration values found', 'CONFIG_QUERY'); } } } // Handle config update responses function handleConfigUpdateResponse(responseData) { console.log('=== CONFIG UPDATE RESPONSE ==='); console.log('Query type:', responseData.query_type); console.log('Status:', responseData.status); console.log('Data:', responseData.data); if (responseData.status === 'success') { const updatesApplied = responseData.updates_applied || 0; log(`Configuration updated successfully: ${updatesApplied} parameters changed`, 'INFO'); // Show success message with details if (responseData.data && Array.isArray(responseData.data)) { responseData.data.forEach((config, index) => { if (config.status === 'success') { log(`✓ ${config.key}: ${config.value} (${config.data_type})`, 'INFO'); } else { log(`✗ ${config.key}: ${config.error || 'Failed to update'}`, 'ERROR'); } }); } // Configuration updated successfully - user can manually refresh using Fetch Config button log('Configuration updated successfully. Click "Fetch Config" to refresh the display.', 'INFO'); } else { const errorMessage = responseData.message || responseData.error || 'Unknown error'; log(`Configuration update failed: ${errorMessage}`, 'ERROR'); // Show detailed error information if available if (responseData.data && Array.isArray(responseData.data)) { responseData.data.forEach((config, index) => { if (config.status === 'error') { log(`✗ ${config.key}: ${config.error || 'Failed to update'}`, 'ERROR'); } }); } } // Log to test interface for debugging if (typeof logTestEvent === 'function') { logTestEvent('RECV', `Config update response: ${responseData.status}`, 'CONFIG_UPDATE'); if (responseData.data && responseData.data.length > 0) { responseData.data.forEach((config, index) => { const status = config.status === 'success' ? '✓' : '✗'; const message = config.status === 'success' ? `${config.key} = ${config.value}` : `${config.key}: ${config.error || 'Failed'}`; logTestEvent('RECV', `${status} ${message}`, 'CONFIG_UPDATE'); }); } else { logTestEvent('RECV', 'No configuration update details received', 'CONFIG_UPDATE'); } } } // Handle auth query responses function handleAuthQueryResponse(responseData) { console.log('=== AUTH QUERY RESPONSE ==='); console.log('Query type:', responseData.query_type); console.log('Total results:', responseData.total_results); console.log('Data:', responseData.data); // Update the current auth rules with the response data if (responseData.data && Array.isArray(responseData.data)) { currentAuthRules = responseData.data; console.log('Updated currentAuthRules with', currentAuthRules.length, 'rules'); // Always show the auth rules table when we receive data (no VIEW RULES button anymore) console.log('Auto-showing auth rules table since we received data...'); showAuthRulesTable(); updateAuthRulesStatus('loaded'); log(`Loaded ${responseData.total_results} auth rules from relay`, 'INFO'); } else { currentAuthRules = []; console.log('No auth rules data received, cleared currentAuthRules'); // Show empty table (no VIEW RULES button anymore) console.log('Auto-showing auth rules table with empty data...'); showAuthRulesTable(); updateAuthRulesStatus('loaded'); log('No auth rules found on relay', 'INFO'); } if (typeof logTestEvent === 'function') { logTestEvent('RECV', `Auth query response: ${responseData.query_type}, ${responseData.total_results} results`, 'AUTH_QUERY'); if (responseData.data && responseData.data.length > 0) { responseData.data.forEach((rule, index) => { logTestEvent('RECV', `Rule ${index + 1}: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'AUTH_RULE'); }); } else { logTestEvent('RECV', 'No auth rules found', 'AUTH_QUERY'); } } } // Handle system command responses function handleSystemCommandResponse(responseData) { console.log('=== SYSTEM COMMAND RESPONSE ==='); console.log('Command:', responseData.command); console.log('Status:', responseData.status); // Handle delete auth rule responses if (responseData.command === 'delete_auth_rule') { if (responseData.status === 'success') { log('Auth rule deleted successfully', 'INFO'); // Refresh the auth rules display loadAuthRules(); } else { log(`Failed to delete auth rule: ${responseData.message || 'Unknown error'}`, 'ERROR'); } } // Handle clear all auth rules responses if (responseData.command === 'clear_all_auth_rules') { if (responseData.status === 'success') { const rulesCleared = responseData.rules_cleared || 0; log(`Successfully cleared ${rulesCleared} auth rules`, 'INFO'); // Clear local auth rules and refresh display currentAuthRules = []; displayAuthRules(currentAuthRules); } else { log(`Failed to clear auth rules: ${responseData.message || 'Unknown error'}`, 'ERROR'); } } if (typeof logTestEvent === 'function') { logTestEvent('RECV', `System command response: ${responseData.command} - ${responseData.status}`, 'SYSTEM_CMD'); } } // Handle auth rule modification responses function handleAuthRuleResponse(responseData) { console.log('=== AUTH RULE MODIFICATION RESPONSE ==='); console.log('Operation:', responseData.operation); console.log('Status:', responseData.status); // Handle auth rule addition/modification responses if (responseData.status === 'success') { const rulesProcessed = responseData.rules_processed || 0; log(`Successfully processed ${rulesProcessed} auth rule modifications`, 'INFO'); // Refresh the auth rules display to show the new rules if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') { loadAuthRules(); } } else { log(`Failed to process auth rule modifications: ${responseData.message || 'Unknown error'}`, 'ERROR'); } if (typeof logTestEvent === 'function') { logTestEvent('RECV', `Auth rule response: ${responseData.operation} - ${responseData.status}`, 'AUTH_RULE'); if (responseData.processed_rules) { responseData.processed_rules.forEach((rule, index) => { logTestEvent('RECV', `Processed rule ${index + 1}: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'AUTH_RULE'); }); } } } // Helper function to decrypt content from relay using NIP-44 async function decryptFromRelay(encryptedContent) { try { console.log('Decrypting content from relay...'); // Get the relay public key for decryption const relayPubkey = getRelayPubkey(); // Use NIP-07 extension's NIP-44 decrypt method if (!window.nostr || !window.nostr.nip44) { throw new Error('NIP-44 decryption not available via NIP-07 extension'); } const decryptedContent = await window.nostr.nip44.decrypt(relayPubkey, encryptedContent); if (!decryptedContent) { throw new Error('NIP-44 decryption returned empty result'); } console.log('Successfully decrypted content from relay'); return decryptedContent; } catch (error) { console.error('NIP-44 decryption failed:', error); throw error; } } // Fetch configuration using admin API async function fetchConfiguration() { try { console.log('=== FETCHING CONFIGURATION VIA ADMIN API ==='); // Require both login and relay connection if (!isLoggedIn || !userPubkey) { throw new Error('Must be logged in to fetch configuration'); } if (!isRelayConnected || !relayPubkey) { throw new Error('Must be connected to relay to fetch configuration. Please use the Relay Connection section first.'); } // First establish subscription to receive responses const subscriptionResult = await subscribeToConfiguration(); if (!subscriptionResult) { throw new Error('Failed to establish admin response subscription'); } // Wait a moment for subscription to be established await new Promise(resolve => setTimeout(resolve, 500)); // Send config query command if logged in if (isLoggedIn && userPubkey && relayPool) { console.log('Sending config query command...'); // Create command array for getting configuration const command_array = ["config_query", "all"]; // Encrypt the command array directly using NIP-44 const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); if (!encrypted_content) { throw new Error('Failed to encrypt command array'); } // Create single kind 23456 admin event const configEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [["p", getRelayPubkey()]], content: encrypted_content }; // Sign the event const signedEvent = await window.nostr.signEvent(configEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } console.log('Config query event signed, publishing...'); // Publish via SimplePool with detailed error diagnostics const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any const results = await Promise.allSettled(publishPromises); // Log detailed publish results for diagnostics let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; console.log(`✅ Relay ${index} (${url}): Event published successfully`); if (typeof logTestEvent === 'function') { logTestEvent('INFO', `Relay ${index} publish success`, 'PUBLISH'); } } else { console.error(`❌ Relay ${index} (${url}): Publish failed:`, result.reason); if (typeof logTestEvent === 'function') { logTestEvent('ERROR', `Relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); } } }); // Throw error if all relays failed if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected the event. Details: ${errorDetails}`); } console.log('Config query command sent successfully - waiting for response...'); } else { console.log('Not logged in - only subscription established for testing'); } return true; } catch (error) { console.error('Failed to fetch configuration:', error); return false; } } function displayConfiguration(event) { try { console.log('=== DISPLAYING CONFIGURATION EVENT ==='); console.log('Event received for display:', event); currentConfig = event; // Clear existing table configTableBody.innerHTML = ''; // Display tags (editable configuration parameters only) console.log(`Processing ${event.tags.length} configuration parameters`); event.tags.forEach((tag, index) => { if (tag.length >= 2) { const row = document.createElement('tr'); const key = tag[0]; const value = tag[1]; // Create editable input for value const valueInput = document.createElement('input'); valueInput.type = 'text'; valueInput.value = value; valueInput.className = 'config-value-input'; valueInput.dataset.key = key; valueInput.dataset.originalValue = value; valueInput.dataset.rowIndex = index; // Create clickable Actions cell const actionsCell = document.createElement('td'); actionsCell.className = 'config-actions-cell'; actionsCell.textContent = 'SAVE'; actionsCell.dataset.key = key; actionsCell.dataset.originalValue = value; actionsCell.dataset.rowIndex = index; // Initially hide the SAVE text actionsCell.style.color = 'transparent'; // Show SAVE text and make clickable when value changes valueInput.addEventListener('input', function() { if (this.value !== this.dataset.originalValue) { actionsCell.style.color = 'var(--primary-color)'; actionsCell.style.cursor = 'pointer'; actionsCell.onclick = () => saveIndividualConfig(key, valueInput.value, valueInput.dataset.originalValue, actionsCell); } else { actionsCell.style.color = 'transparent'; actionsCell.style.cursor = 'default'; actionsCell.onclick = null; } }); row.innerHTML = `${key}`; row.cells[1].appendChild(valueInput); row.appendChild(actionsCell); configTableBody.appendChild(row); } }); // Show message if no configuration parameters found if (event.tags.length === 0) { const row = document.createElement('tr'); row.innerHTML = `No configuration parameters found`; configTableBody.appendChild(row); } console.log('Configuration display completed successfully'); updateConfigStatus(true); } catch (error) { console.error('Error in displayConfiguration:', error.message); console.error('Display configuration error:', error); } } // Save individual configuration parameter async function saveIndividualConfig(key, newValue, originalValue, actionsCell) { if (!isLoggedIn || !userPubkey) { log('Must be logged in to save configuration', 'ERROR'); return; } if (!currentConfig) { log('No current configuration to update', 'ERROR'); return; } // Don't save if value hasn't changed if (newValue === originalValue) { return; } try { log(`Saving individual config: ${key} = ${newValue}`, 'INFO'); // Determine data type based on key name let dataType = 'string'; if (['max_connections', 'pow_min_difficulty', 'nip42_challenge_timeout', 'max_subscriptions_per_client', 'max_event_tags', 'max_content_length'].includes(key)) { dataType = 'integer'; } else if (['auth_enabled', 'nip42_auth_required', 'nip40_expiration_enabled'].includes(key)) { dataType = 'boolean'; } // Determine category based on key name let category = 'general'; if (key.startsWith('relay_')) { category = 'relay'; } else if (key.startsWith('nip40_')) { category = 'expiration'; } else if (key.startsWith('nip42_') || key.startsWith('auth_')) { category = 'authentication'; } else if (key.startsWith('pow_')) { category = 'proof_of_work'; } else if (key.startsWith('max_')) { category = 'limits'; } const configObj = { key: key, value: newValue, data_type: dataType, category: category }; // Update cell during save actionsCell.textContent = 'SAVING...'; actionsCell.style.color = 'var(--accent-color)'; actionsCell.style.cursor = 'not-allowed'; actionsCell.onclick = null; // Send single config update await sendConfigUpdateCommand([configObj]); // Update the original value on success const input = actionsCell.parentElement.cells[1].querySelector('input'); if (input) { input.dataset.originalValue = newValue; // Hide SAVE text since value now matches original actionsCell.style.color = 'transparent'; actionsCell.style.cursor = 'default'; actionsCell.onclick = null; } actionsCell.textContent = 'SAVED'; actionsCell.style.color = 'var(--accent-color)'; setTimeout(() => { actionsCell.textContent = 'SAVE'; // Keep transparent if value matches original if (input && input.value === input.dataset.originalValue) { actionsCell.style.color = 'transparent'; } }, 2000); log(`Successfully saved config: ${key} = ${newValue}`, 'INFO'); } catch (error) { log(`Failed to save individual config ${key}: ${error.message}`, 'ERROR'); actionsCell.textContent = 'SAVE'; actionsCell.style.color = 'var(--primary-color)'; actionsCell.style.cursor = 'pointer'; actionsCell.onclick = () => saveIndividualConfig(key, actionsCell.parentElement.cells[1].querySelector('input').value, originalValue, actionsCell); } } // Send config update command using kind 23456 with Administrator API (inner events) async function sendConfigUpdateCommand(configObjects) { try { if (!relayPool) { throw new Error('SimplePool connection not available'); } console.log(`Sending config_update command with ${configObjects.length} configuration object(s)`); // Create command array for config update const command_array = ["config_update", configObjects]; // Encrypt the command array directly using NIP-44 const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); if (!encrypted_content) { throw new Error('Failed to encrypt command array'); } // Create single kind 23456 admin event const configEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [["p", getRelayPubkey()]], content: encrypted_content }; // Sign the event const signedEvent = await window.nostr.signEvent(configEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } console.log(`Config update event signed with ${configObjects.length} object(s)`); // Publish via SimplePool with detailed error diagnostics const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any const results = await Promise.allSettled(publishPromises); // Log detailed publish results for diagnostics let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; console.log(`✅ Config Update Relay ${index} (${url}): Event published successfully`); if (typeof logTestEvent === 'function') { logTestEvent('INFO', `Config update relay ${index} publish success`, 'PUBLISH'); } } else { console.error(`❌ Config Update Relay ${index} (${url}): Publish failed:`, result.reason); if (typeof logTestEvent === 'function') { logTestEvent('ERROR', `Config update relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); } } }); // Throw error if all relays failed if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected config update event. Details: ${errorDetails}`); } console.log(`Config update command sent successfully with ${configObjects.length} configuration object(s)`); // Log for testing if (typeof logTestEvent === 'function') { logTestEvent('SENT', `Config update command: ${configObjects.length} object(s)`, 'CONFIG_UPDATE'); configObjects.forEach((config, index) => { logTestEvent('SENT', `Config ${index + 1}: ${config.key} = ${config.value} (${config.data_type})`, 'CONFIG'); }); } } catch (error) { console.error(`Failed to send config_update command:`, error); throw error; } } // Login/Logout button functionality function updateLoginLogoutButton() { const loginLogoutBtn = document.getElementById('login-logout-btn'); if (!loginLogoutBtn) return; if (isLoggedIn) { loginLogoutBtn.textContent = 'LOGOUT'; loginLogoutBtn.className = 'login-logout-btn logout-state'; loginLogoutBtn.onclick = logout; // Show user details when logged in if (persistentUserDetails) { persistentUserDetails.style.display = 'block'; } } else { loginLogoutBtn.textContent = 'ADMIN NOSTR LOGIN'; loginLogoutBtn.className = 'login-logout-btn'; loginLogoutBtn.onclick = () => { if (window.NOSTR_LOGIN_LITE && window.NOSTR_LOGIN_LITE.launch) { window.NOSTR_LOGIN_LITE.launch('login'); } else { console.log('NOSTR_LOGIN_LITE not available'); } }; // Hide user details when logged out if (persistentUserDetails) { persistentUserDetails.style.display = 'none'; } } } // Event handlers // Initialize login/logout button updateLoginLogoutButton(); fetchConfigBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); fetchConfiguration().catch(error => { console.log('Manual fetch configuration failed: ' + error.message); }); }); // Relay connection event handlers connectRelayBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); connectToRelay().catch(error => { console.log('Relay connection failed: ' + error.message); }); }); disconnectRelayBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); disconnectFromRelay(); }); restartRelayBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); sendRestartCommand().catch(error => { log(`Restart command failed: ${error.message}`, 'ERROR'); }); }); // ================================ // AUTH RULES MANAGEMENT FUNCTIONS // ================================ // Global auth rules state let currentAuthRules = []; let editingAuthRule = null; // DOM elements for auth rules const authRulesSection = document.getElementById('authRulesSection'); const refreshAuthRulesBtn = document.getElementById('refreshAuthRulesBtn'); const authRulesTableContainer = document.getElementById('authRulesTableContainer'); const authRulesTableBody = document.getElementById('authRulesTableBody'); const authRuleFormContainer = document.getElementById('authRuleFormContainer'); const authRuleForm = document.getElementById('authRuleForm'); const authRuleFormTitle = document.getElementById('authRuleFormTitle'); const saveAuthRuleBtn = document.getElementById('saveAuthRuleBtn'); const cancelAuthRuleBtn = document.getElementById('cancelAuthRuleBtn'); // Show auth rules section after login function showAuthRulesSection() { if (authRulesSection) { authRulesSection.style.display = 'block'; updateAuthRulesStatus('ready'); log('Auth rules section is now available', 'INFO'); } } // Hide auth rules section on logout function hideAuthRulesSection() { if (authRulesSection) { authRulesSection.style.display = 'none'; // Add null checks for all elements if (authRulesTableContainer) { authRulesTableContainer.style.display = 'none'; } if (authRuleFormContainer) { authRuleFormContainer.style.display = 'none'; } currentAuthRules = []; editingAuthRule = null; log('Auth rules section hidden', 'INFO'); } } // Update auth rules status indicator (removed - no status element) function updateAuthRulesStatus(status) { // Status element removed - no-op } // Load auth rules from relay using admin API async function loadAuthRules() { try { log('Loading auth rules via admin API...', 'INFO'); updateAuthRulesStatus('loading'); if (!isLoggedIn || !userPubkey) { throw new Error('Must be logged in to load auth rules'); } if (!relayPool) { throw new Error('SimplePool connection not available'); } // Create command array for getting all auth rules const command_array = ["auth_query", "all"]; // Encrypt the command array directly using NIP-44 const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); if (!encrypted_content) { throw new Error('Failed to encrypt command array'); } // Create single kind 23456 admin event const authEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [["p", getRelayPubkey()]], content: encrypted_content }; // Sign the event const signedEvent = await window.nostr.signEvent(authEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } log('Sending auth rules query to relay...', 'INFO'); // Publish via SimplePool with detailed error diagnostics const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any const results = await Promise.allSettled(publishPromises); // Log detailed publish results for diagnostics let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; console.log(`✅ Auth Rules Query Relay ${index} (${url}): Event published successfully`); if (typeof logTestEvent === 'function') { logTestEvent('INFO', `Auth rules query relay ${index} publish success`, 'PUBLISH'); } } else { console.error(`❌ Auth Rules Query Relay ${index} (${url}): Publish failed:`, result.reason); if (typeof logTestEvent === 'function') { logTestEvent('ERROR', `Auth rules query relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); } } }); // Throw error if all relays failed if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected auth rules query event. Details: ${errorDetails}`); } log('Auth rules query sent successfully - waiting for response...', 'INFO'); updateAuthRulesStatus('loaded'); } catch (error) { log(`Failed to load auth rules: ${error.message}`, 'ERROR'); updateAuthRulesStatus('error'); currentAuthRules = []; displayAuthRules(currentAuthRules); } } // Display auth rules in the table function displayAuthRules(rules) { console.log('=== DISPLAY AUTH RULES DEBUG ==='); console.log('authRulesTableBody element:', authRulesTableBody); console.log('Rules to display:', rules); console.log('Rules length:', rules ? rules.length : 'undefined'); console.log('authRulesTableContainer display:', authRulesTableContainer ? authRulesTableContainer.style.display : 'element not found'); if (!authRulesTableBody) { console.log('ERROR: authRulesTableBody element not found'); return; } authRulesTableBody.innerHTML = ''; console.log('Cleared existing table content'); if (!rules || rules.length === 0) { console.log('No rules to display, showing empty message'); const row = document.createElement('tr'); row.innerHTML = `No auth rules configured`; authRulesTableBody.appendChild(row); console.log('Added empty rules message row'); return; } console.log(`Displaying ${rules.length} auth rules`); rules.forEach((rule, index) => { console.log(`Adding rule ${index + 1}:`, rule); const row = document.createElement('tr'); row.innerHTML = ` ${rule.rule_type} ${rule.pattern_type || rule.operation || '-'} ${rule.pattern_value || rule.rule_target || '-'} ${rule.action || 'allow'} ${rule.enabled !== false ? 'Active' : 'Inactive'}
`; authRulesTableBody.appendChild(row); }); // Update status display console.log(`Total Rules: ${rules.length}, Active Rules: ${rules.filter(r => r.enabled !== false).length}`); console.log('=== END DISPLAY AUTH RULES DEBUG ==='); } // Show auth rules table (automatically called when auth rules are loaded) function showAuthRulesTable() { console.log('=== SHOW AUTH RULES TABLE DEBUG ==='); console.log('authRulesTableContainer element:', authRulesTableContainer); console.log('Current display style:', authRulesTableContainer ? authRulesTableContainer.style.display : 'element not found'); if (authRulesTableContainer) { authRulesTableContainer.style.display = 'block'; console.log('Set authRulesTableContainer display to block'); // If we already have cached auth rules, display them immediately if (currentAuthRules && currentAuthRules.length >= 0) { console.log('Displaying cached auth rules:', currentAuthRules.length, 'rules'); displayAuthRules(currentAuthRules); updateAuthRulesStatus('loaded'); log(`Auth rules table displayed with ${currentAuthRules.length} cached rules`, 'INFO'); } else { // No cached rules, load from relay console.log('No cached auth rules, loading from relay...'); loadAuthRules(); log('Auth rules table displayed - loading from relay', 'INFO'); } } else { console.log('ERROR: authRulesTableContainer element not found'); } console.log('=== END SHOW AUTH RULES TABLE DEBUG ==='); } // Show add auth rule form function showAddAuthRuleForm() { if (authRuleFormContainer && authRuleFormTitle) { editingAuthRule = null; authRuleFormTitle.textContent = 'Add Auth Rule'; authRuleForm.reset(); authRuleFormContainer.style.display = 'block'; log('Opened add auth rule form', 'INFO'); } } // Show edit auth rule form function editAuthRule(index) { if (index < 0 || index >= currentAuthRules.length) return; const rule = currentAuthRules[index]; editingAuthRule = { ...rule, index: index }; if (authRuleFormTitle && authRuleForm) { authRuleFormTitle.textContent = 'Edit Auth Rule'; // Populate form fields document.getElementById('authRuleType').value = rule.rule_type || ''; document.getElementById('authPatternType').value = rule.pattern_type || rule.operation || ''; document.getElementById('authPatternValue').value = rule.pattern_value || rule.rule_target || ''; document.getElementById('authRuleAction').value = rule.action || 'allow'; document.getElementById('authRuleDescription').value = rule.description || ''; authRuleFormContainer.style.display = 'block'; log(`Editing auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO'); } } // Delete auth rule using Administrator API (inner events) async function deleteAuthRule(index) { if (index < 0 || index >= currentAuthRules.length) return; const rule = currentAuthRules[index]; const confirmMsg = `Delete auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}?`; if (!confirm(confirmMsg)) return; try { log(`Deleting auth rule: ${rule.rule_type} - ${rule.pattern_value || rule.rule_target}`, 'INFO'); if (!isLoggedIn || !userPubkey) { throw new Error('Must be logged in to delete auth rules'); } if (!relayPool) { throw new Error('SimplePool connection not available'); } // Create command array for deleting auth rule // Format: ["system_command", "delete_auth_rule", rule_type, pattern_type, pattern_value] const rule_type = rule.rule_type; const pattern_type = rule.pattern_type || 'pubkey'; const pattern_value = rule.pattern_value || rule.rule_target; const command_array = ["system_command", "delete_auth_rule", rule_type, pattern_type, pattern_value]; // Encrypt the command array directly using NIP-44 const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); if (!encrypted_content) { throw new Error('Failed to encrypt command array'); } // Create single kind 23456 admin event const authEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [["p", getRelayPubkey()]], content: encrypted_content }; // Sign the event const signedEvent = await window.nostr.signEvent(authEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } log('Sending delete auth rule command to relay...', 'INFO'); // Publish via SimplePool with detailed error diagnostics const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any const results = await Promise.allSettled(publishPromises); // Log detailed publish results for diagnostics let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; console.log(`✅ Delete Auth Rule Relay ${index} (${url}): Event published successfully`); if (typeof logTestEvent === 'function') { logTestEvent('INFO', `Delete auth rule relay ${index} publish success`, 'PUBLISH'); } } else { console.error(`❌ Delete Auth Rule Relay ${index} (${url}): Publish failed:`, result.reason); if (typeof logTestEvent === 'function') { logTestEvent('ERROR', `Delete auth rule relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); } } }); // Throw error if all relays failed if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected delete auth rule event. Details: ${errorDetails}`); } log('Delete auth rule command sent successfully - waiting for response...', 'INFO'); // Remove from local array immediately for UI responsiveness currentAuthRules.splice(index, 1); displayAuthRules(currentAuthRules); } catch (error) { log(`Failed to delete auth rule: ${error.message}`, 'ERROR'); } } // Hide auth rule form function hideAuthRuleForm() { if (authRuleFormContainer) { authRuleFormContainer.style.display = 'none'; editingAuthRule = null; log('Auth rule form hidden', 'INFO'); } } // Validate auth rule form function validateAuthRuleForm() { const ruleType = document.getElementById('authRuleType').value; const patternType = document.getElementById('authPatternType').value; const patternValue = document.getElementById('authPatternValue').value.trim(); const action = document.getElementById('authRuleAction').value; if (!ruleType) { alert('Please select a rule type'); return false; } if (!patternType) { alert('Please select a pattern type'); return false; } if (!patternValue) { alert('Please enter a pattern value'); return false; } if (!action) { alert('Please select an action'); return false; } // Validate pubkey format for pubkey rules if ((ruleType === 'pubkey_whitelist' || ruleType === 'pubkey_blacklist') && patternValue.length !== 64) { alert('Pubkey must be exactly 64 hex characters'); return false; } // Validate hex format for pubkey rules if ((ruleType === 'pubkey_whitelist' || ruleType === 'pubkey_blacklist')) { const hexPattern = /^[0-9a-fA-F]+$/; if (!hexPattern.test(patternValue)) { alert('Pubkey must contain only hex characters (0-9, a-f, A-F)'); return false; } } return true; } // Save auth rule (add or update) async function saveAuthRule(event) { event.preventDefault(); if (!validateAuthRuleForm()) return; try { const ruleData = { rule_type: document.getElementById('authRuleType').value, pattern_type: document.getElementById('authPatternType').value, pattern_value: document.getElementById('authPatternValue').value.trim(), action: document.getElementById('authRuleAction').value, description: document.getElementById('authRuleDescription').value.trim() || null, enabled: true }; if (editingAuthRule) { log(`Updating auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO'); // TODO: Implement actual rule update via WebSocket kind 23456 event // For now, just update local array currentAuthRules[editingAuthRule.index] = { ...ruleData, id: editingAuthRule.id || Date.now() }; log('Auth rule updated (placeholder implementation)', 'INFO'); } else { log(`Adding new auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value}`, 'INFO'); // TODO: Implement actual rule creation via WebSocket kind 23456 event // For now, just add to local array currentAuthRules.push({ ...ruleData, id: Date.now() }); log('Auth rule added (placeholder implementation)', 'INFO'); } displayAuthRules(currentAuthRules); hideAuthRuleForm(); } catch (error) { log(`Failed to save auth rule: ${error.message}`, 'ERROR'); } } // Update existing logout and showMainInterface functions to handle auth rules and NIP-17 DMs const originalLogout = logout; logout = async function () { hideAuthRulesSection(); // Clear DM inbox and outbox on logout if (dmInbox) { dmInbox.innerHTML = '
No messages received yet.
'; } if (dmOutbox) { dmOutbox.value = ''; } await originalLogout(); }; const originalShowMainInterface = showMainInterface; showMainInterface = function () { originalShowMainInterface(); showAuthRulesSection(); }; // Auth rules event handlers if (refreshAuthRulesBtn) { refreshAuthRulesBtn.addEventListener('click', function (e) { e.preventDefault(); loadAuthRules(); }); } if (authRuleForm) { authRuleForm.addEventListener('submit', saveAuthRule); } if (cancelAuthRuleBtn) { cancelAuthRuleBtn.addEventListener('click', function (e) { e.preventDefault(); hideAuthRuleForm(); }); } // ================================ // STREAMLINED AUTH RULE FUNCTIONS // ================================ // Utility function to convert nsec to hex pubkey function nsecToHex(input) { if (!input || input.trim().length === 0) { return null; } const trimmed = input.trim(); // If it's already 64-char hex, return as-is if (/^[0-9a-fA-F]{64}$/.test(trimmed)) { return trimmed; } // If it starts with nsec1, try to decode if (trimmed.startsWith('nsec1')) { try { if (window.NostrTools && window.NostrTools.nip19 && window.NostrTools.nip19.decode) { const decoded = window.NostrTools.nip19.decode(trimmed); if (decoded.type === 'nsec') { // Convert bytes to hex const hexPubkey = Array.from(decoded.data) .map(b => b.toString(16).padStart(2, '0')) .join(''); return hexPubkey; } } } catch (error) { console.error('Failed to decode nsec:', error); return null; } } return null; // Invalid format } // Add blacklist rule (updated to use combined input) function addBlacklistRule() { const input = document.getElementById('authRulePubkey'); if (!input) return; const inputValue = input.value.trim(); if (!inputValue) { log('Please enter a pubkey or nsec', 'ERROR'); return; } // Convert nsec to hex if needed const hexPubkey = nsecToHex(inputValue); if (!hexPubkey) { log('Invalid pubkey format. Please enter nsec1... or 64-character hex', 'ERROR'); return; } // Validate hex length if (hexPubkey.length !== 64) { log('Invalid pubkey length. Must be exactly 64 characters', 'ERROR'); return; } log('Adding to blacklist...', 'INFO'); // Create auth rule data const ruleData = { rule_type: 'pubkey_blacklist', pattern_type: 'Global', pattern_value: hexPubkey, action: 'deny' }; // Add to WebSocket queue for processing addAuthRuleViaWebSocket(ruleData) .then(() => { log(`Pubkey ${hexPubkey.substring(0, 16)}... added to blacklist`, 'INFO'); input.value = ''; // Refresh auth rules display if visible if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') { loadAuthRules(); } }) .catch(error => { log(`Failed to add rule: ${error.message}`, 'ERROR'); }); } // Add whitelist rule (updated to use combined input) function addWhitelistRule() { const input = document.getElementById('authRulePubkey'); const warningDiv = document.getElementById('whitelistWarning'); if (!input) return; const inputValue = input.value.trim(); if (!inputValue) { log('Please enter a pubkey or nsec', 'ERROR'); return; } // Convert nsec to hex if needed const hexPubkey = nsecToHex(inputValue); if (!hexPubkey) { log('Invalid pubkey format. Please enter nsec1... or 64-character hex', 'ERROR'); return; } // Validate hex length if (hexPubkey.length !== 64) { log('Invalid pubkey length. Must be exactly 64 characters', 'ERROR'); return; } // Show whitelist warning if (warningDiv) { warningDiv.style.display = 'block'; } log('Adding to whitelist...', 'INFO'); // Create auth rule data const ruleData = { rule_type: 'pubkey_whitelist', pattern_type: 'Global', pattern_value: hexPubkey, action: 'allow' }; // Add to WebSocket queue for processing addAuthRuleViaWebSocket(ruleData) .then(() => { log(`Pubkey ${hexPubkey.substring(0, 16)}... added to whitelist`, 'INFO'); input.value = ''; // Refresh auth rules display if visible if (authRulesTableContainer && authRulesTableContainer.style.display !== 'none') { loadAuthRules(); } }) .catch(error => { log(`Failed to add rule: ${error.message}`, 'ERROR'); }); } // Add auth rule via SimplePool (kind 23456 event) - FIXED to match working test pattern async function addAuthRuleViaWebSocket(ruleData) { if (!isLoggedIn || !userPubkey) { throw new Error('Must be logged in to add auth rules'); } if (!relayPool) { throw new Error('SimplePool connection not available'); } try { log(`Adding auth rule: ${ruleData.rule_type} - ${ruleData.pattern_value.substring(0, 16)}...`, 'INFO'); // Map client-side rule types to command array format (matching working tests) let commandRuleType, commandPatternType; switch (ruleData.rule_type) { case 'pubkey_blacklist': commandRuleType = 'blacklist'; commandPatternType = 'pubkey'; break; case 'pubkey_whitelist': commandRuleType = 'whitelist'; commandPatternType = 'pubkey'; break; case 'hash_blacklist': commandRuleType = 'blacklist'; commandPatternType = 'hash'; break; default: throw new Error(`Unknown rule type: ${ruleData.rule_type}`); } // Create command array in the same format as working tests // Format: ["blacklist", "pubkey", "abc123..."] or ["whitelist", "pubkey", "def456..."] const command_array = [commandRuleType, commandPatternType, ruleData.pattern_value]; // Encrypt the command array directly using NIP-44 const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); if (!encrypted_content) { throw new Error('Failed to encrypt command array'); } // Create single kind 23456 admin event const authEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [["p", getRelayPubkey()]], content: encrypted_content }; // DEBUG: Log the complete event structure being sent console.log('=== AUTH RULE EVENT DEBUG (Administrator API) ==='); console.log('Original Rule Data:', ruleData); console.log('Command Array:', command_array); console.log('Encrypted Content:', encrypted_content.substring(0, 50) + '...'); console.log('Auth Event (before signing):', JSON.stringify(authEvent, null, 2)); console.log('=== END AUTH RULE EVENT DEBUG ==='); // Sign the event const signedEvent = await window.nostr.signEvent(authEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } // Publish via SimplePool with detailed error diagnostics const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any const results = await Promise.allSettled(publishPromises); // Log detailed publish results for diagnostics let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; console.log(`✅ Add Auth Rule Relay ${index} (${url}): Event published successfully`); if (typeof logTestEvent === 'function') { logTestEvent('INFO', `Add auth rule relay ${index} publish success`, 'PUBLISH'); } } else { console.error(`❌ Add Auth Rule Relay ${index} (${url}): Publish failed:`, result.reason); if (typeof logTestEvent === 'function') { logTestEvent('ERROR', `Add auth rule relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); } } }); // Throw error if all relays failed if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected add auth rule event. Details: ${errorDetails}`); } log('Auth rule added successfully', 'INFO'); } catch (error) { log(`Failed to add auth rule: ${error.message}`, 'ERROR'); throw error; } } // ================================ // TEST FUNCTIONS FOR ADMIN API // ================================ // Test event logging function function logTestEvent(direction, message, type = 'INFO') { const testLog = document.getElementById('test-event-log'); if (!testLog) return; const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; const logEntry = document.createElement('div'); logEntry.className = 'log-entry'; const directionColor = direction === 'SENT' ? '#007bff' : '#28a745'; logEntry.innerHTML = ` ${timestamp} [${direction}] [${type}] ${message} `; testLog.appendChild(logEntry); testLog.scrollTop = testLog.scrollHeight; } // Test: Get Auth Rules async function testGetAuthRules() { if (!isLoggedIn || !userPubkey) { logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR'); return; } if (!relayPool) { logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR'); return; } try { logTestEvent('INFO', 'Testing Get Auth Rules command...', 'TEST'); // Create command array for getting auth rules const command_array = '["auth_query", "all"]'; // Encrypt the command content using NIP-44 const encrypted_content = await encryptForRelay(command_array); if (!encrypted_content) { throw new Error('Failed to encrypt auth query command'); } // Create kind 23456 admin event const authEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [ ["p", getRelayPubkey()] ], content: encrypted_content }; // Sign the event const signedEvent = await window.nostr.signEvent(authEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } logTestEvent('SENT', `Get Auth Rules event: ${JSON.stringify(signedEvent)}`, 'EVENT'); // Publish via SimplePool with detailed error diagnostics const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any const results = await Promise.allSettled(publishPromises); // Log detailed publish results for diagnostics let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; logTestEvent('INFO', `Test Add Blacklist relay ${index} publish success`, 'PUBLISH'); } else { logTestEvent('ERROR', `Test Add Blacklist relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); } }); // Throw error if all relays failed if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected test add blacklist event. Details: ${errorDetails}`); } logTestEvent('INFO', 'Get Auth Rules command sent successfully', 'SUCCESS'); } catch (error) { logTestEvent('ERROR', `Get Auth Rules test failed: ${error.message}`, 'ERROR'); } } // Test: Clear All Auth Rules async function testClearAuthRules() { if (!isLoggedIn || !userPubkey) { logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR'); return; } if (!relayPool) { logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR'); return; } try { logTestEvent('INFO', 'Testing Clear All Auth Rules command...', 'TEST'); // Create command array for clearing auth rules const command_array = '["system_command", "clear_all_auth_rules"]'; // Encrypt the command content using NIP-44 const encrypted_content = await encryptForRelay(command_array); if (!encrypted_content) { throw new Error('Failed to encrypt clear auth rules command'); } // Create kind 23456 admin event const authEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [ ["p", getRelayPubkey()] ], content: encrypted_content }; // Sign the event const signedEvent = await window.nostr.signEvent(authEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } logTestEvent('SENT', `Clear Auth Rules event: ${JSON.stringify(signedEvent)}`, 'EVENT'); // Publish via SimplePool with detailed error diagnostics const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any const results = await Promise.allSettled(publishPromises); // Log detailed publish results for diagnostics let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; logTestEvent('INFO', `Test Add Whitelist relay ${index} publish success`, 'PUBLISH'); } else { logTestEvent('ERROR', `Test Add Whitelist relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); } }); // Throw error if all relays failed if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected test add whitelist event. Details: ${errorDetails}`); } logTestEvent('INFO', 'Clear Auth Rules command sent successfully', 'SUCCESS'); } catch (error) { logTestEvent('ERROR', `Clear Auth Rules test failed: ${error.message}`, 'ERROR'); } } // Test: Add Blacklist async function testAddBlacklist() { const testPubkeyInput = document.getElementById('test-pubkey-input'); let testPubkey = testPubkeyInput ? testPubkeyInput.value.trim() : ''; // Use a default test pubkey if none provided if (!testPubkey) { testPubkey = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; logTestEvent('INFO', `Using default test pubkey: ${testPubkey}`, 'INFO'); } if (!isLoggedIn || !userPubkey) { logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR'); return; } if (!relayPool) { logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR'); return; } try { logTestEvent('INFO', `Testing Add Blacklist for pubkey: ${testPubkey.substring(0, 16)}...`, 'TEST'); // Create command array for adding blacklist rule const command_array = `["blacklist", "pubkey", "${testPubkey}"]`; // Encrypt the command content using NIP-44 const encrypted_content = await encryptForRelay(command_array); if (!encrypted_content) { throw new Error('Failed to encrypt blacklist command'); } // Create kind 23456 admin event const authEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [ ["p", getRelayPubkey()] ], content: encrypted_content }; // Sign the event const signedEvent = await window.nostr.signEvent(authEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } logTestEvent('SENT', `Add Blacklist event: ${JSON.stringify(signedEvent)}`, 'EVENT'); // Publish via SimplePool with detailed error diagnostics const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any const results = await Promise.allSettled(publishPromises); // Log detailed publish results for diagnostics let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; logTestEvent('INFO', `Test Config Query relay ${index} publish success`, 'PUBLISH'); } else { logTestEvent('ERROR', `Test Config Query relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); } }); // Throw error if all relays failed if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected test config query event. Details: ${errorDetails}`); } logTestEvent('INFO', 'Add Blacklist command sent successfully', 'SUCCESS'); } catch (error) { logTestEvent('ERROR', `Add Blacklist test failed: ${error.message}`, 'ERROR'); } } // Test: Add Whitelist async function testAddWhitelist() { const testPubkeyInput = document.getElementById('test-pubkey-input'); let testPubkey = testPubkeyInput ? testPubkeyInput.value.trim() : ''; // Use a default test pubkey if none provided if (!testPubkey) { testPubkey = 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'; logTestEvent('INFO', `Using default test pubkey: ${testPubkey}`, 'INFO'); } if (!isLoggedIn || !userPubkey) { logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR'); return; } if (!relayPool) { logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR'); return; } try { logTestEvent('INFO', `Testing Add Whitelist for pubkey: ${testPubkey.substring(0, 16)}...`, 'TEST'); // Create command array for adding whitelist rule const command_array = `["whitelist", "pubkey", "${testPubkey}"]`; // Encrypt the command content using NIP-44 const encrypted_content = await encryptForRelay(command_array); if (!encrypted_content) { throw new Error('Failed to encrypt whitelist command'); } // Create kind 23456 admin event const authEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [ ["p", getRelayPubkey()] ], content: encrypted_content }; // Sign the event const signedEvent = await window.nostr.signEvent(authEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } logTestEvent('SENT', `Add Whitelist event: ${JSON.stringify(signedEvent)}`, 'EVENT'); // Publish via SimplePool const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any const results = await Promise.allSettled(publishPromises); // Log detailed publish results for diagnostics let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; logTestEvent('INFO', `Test Post Event relay ${index} publish success`, 'PUBLISH'); } else { logTestEvent('ERROR', `Test Post Event relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); } }); // Throw error if all relays failed if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected test post event. Details: ${errorDetails}`); } logTestEvent('INFO', 'Add Whitelist command sent successfully', 'SUCCESS'); } catch (error) { logTestEvent('ERROR', `Add Whitelist test failed: ${error.message}`, 'ERROR'); } } // Test: Config Query async function testConfigQuery() { if (!isLoggedIn || !userPubkey) { logTestEvent('ERROR', 'Must be logged in to test admin API', 'ERROR'); return; } if (!relayPool) { logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR'); return; } try { logTestEvent('INFO', 'Testing Config Query command...', 'TEST'); // Create command array for getting configuration const command_array = '["config_query", "all"]'; // Encrypt the command content using NIP-44 const encrypted_content = await encryptForRelay(command_array); if (!encrypted_content) { throw new Error('Failed to encrypt config query command'); } // Create kind 23456 admin event const configEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [ ["p", getRelayPubkey()] ], content: encrypted_content }; // Sign the event const signedEvent = await window.nostr.signEvent(configEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } logTestEvent('SENT', `Config Query event: ${JSON.stringify(signedEvent)}`, 'EVENT'); // Publish via SimplePool with detailed error diagnostics const url = relayUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any const results = await Promise.allSettled(publishPromises); // Log detailed publish results for diagnostics let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; logTestEvent('INFO', `Test Config Query relay ${index} publish success`, 'PUBLISH'); } else { logTestEvent('ERROR', `Test Config Query relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); } }); // Throw error if all relays failed if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected test config query event. Details: ${errorDetails}`); } logTestEvent('INFO', 'Config Query command sent successfully', 'SUCCESS'); } catch (error) { logTestEvent('ERROR', `Config Query test failed: ${error.message}`, 'ERROR'); } } // Test: Post Basic Event async function testPostEvent() { if (!isLoggedIn || !userPubkey) { logTestEvent('ERROR', 'Must be logged in to test event posting', 'ERROR'); return; } if (!relayPool) { logTestEvent('ERROR', 'SimplePool connection not available', 'ERROR'); return; } try { logTestEvent('INFO', 'Testing basic event posting...', 'TEST'); // Create a simple kind 1 text note event const testEvent = { kind: 1, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [ ["t", "test"], ["client", "c-relay-admin-api"] ], content: `Test event from C-Relay Admin API at ${new Date().toISOString()}` }; logTestEvent('SENT', `Test event (before signing): ${JSON.stringify(testEvent)}`, 'EVENT'); // Sign the event using NIP-07 const signedEvent = await window.nostr.signEvent(testEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } logTestEvent('SENT', `Signed test event: ${JSON.stringify(signedEvent)}`, 'EVENT'); // Publish via SimplePool to the same relay with detailed error diagnostics const url = relayConnectionUrl.value.trim(); logTestEvent('INFO', `Publishing to relay: ${url}`, 'INFO'); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes instead of Promise.any const results = await Promise.allSettled(publishPromises); // Log detailed publish results for diagnostics let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; logTestEvent('INFO', `Test Post Event relay ${index} publish success`, 'PUBLISH'); } else { logTestEvent('ERROR', `Test Post Event relay ${index} publish failed: ${result.reason?.message || result.reason}`, 'PUBLISH'); } }); // Throw error if all relays failed if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected test post event. Details: ${errorDetails}`); } logTestEvent('INFO', 'Test event published successfully!', 'SUCCESS'); logTestEvent('INFO', 'Check if the event appears in the subscription above...', 'INFO'); } catch (error) { logTestEvent('ERROR', `Post Event test failed: ${error.message}`, 'ERROR'); console.error('Post Event test error:', error); } } // Helper function to encrypt content for relay using NIP-44 async function encryptForRelay(content) { try { logTestEvent('INFO', `Encrypting content: ${content}`, 'DEBUG'); // Get the relay public key for encryption const relayPubkey = getRelayPubkey(); // Check if we have access to NIP-44 encryption via nostr-tools if (!window.NostrTools || !window.NostrTools.nip44) { throw new Error('NIP-44 encryption not available - nostr-tools library missing'); } // Get user's private key for encryption // We need to use the NIP-07 extension to get the private key if (!window.nostr || !window.nostr.nip44) { throw new Error('NIP-44 encryption not available via NIP-07 extension'); } // Use NIP-07 extension's NIP-44 encrypt method const encrypted_content = await window.nostr.nip44.encrypt(relayPubkey, content); if (!encrypted_content) { throw new Error('NIP-44 encryption returned empty result'); } logTestEvent('INFO', `Successfully encrypted content using NIP-44`, 'DEBUG'); logTestEvent('INFO', `Encrypted content: ${encrypted_content.substring(0, 50)}...`, 'DEBUG'); return encrypted_content; } catch (error) { logTestEvent('ERROR', `NIP-44 encryption failed: ${error.message}`, 'ERROR'); // Fallback: Try using nostr-tools directly if NIP-07 fails try { logTestEvent('INFO', 'Attempting fallback encryption with nostr-tools...', 'DEBUG'); if (!window.NostrTools || !window.NostrTools.nip44) { throw new Error('nostr-tools NIP-44 not available'); } // We need the user's private key, but we can't get it directly // This is a security limitation - we should use NIP-07 throw new Error('Cannot access private key for direct encryption - use NIP-07 extension'); } catch (fallbackError) { logTestEvent('ERROR', `Fallback encryption failed: ${fallbackError.message}`, 'ERROR'); return null; } } } // Send NIP-17 Direct Message to relay using NIP-59 layering async function sendNIP17DM() { if (!isLoggedIn || !userPubkey) { log('Must be logged in to send DM', 'ERROR'); return; } if (!isRelayConnected || !relayPubkey) { log('Must be connected to relay to send DM', 'ERROR'); return; } const message = dmOutbox.value.trim(); if (!message) { log('Please enter a message to send', 'ERROR'); return; } // Capability checks if (!window.nostr || !window.nostr.nip44 || !window.nostr.signEvent) { log('NIP-17 DMs require a NIP-07 extension with NIP-44 support', 'ERROR'); alert('NIP-17 DMs require a NIP-07 extension with NIP-44 support. Please install and configure a compatible extension.'); return; } if (!window.NostrTools || !window.NostrTools.generateSecretKey || !window.NostrTools.getPublicKey || !window.NostrTools.finalizeEvent) { log('NostrTools library not available for ephemeral key operations', 'ERROR'); alert('NostrTools library not available. Please ensure nostr.bundle.js is loaded.'); return; } try { log(`Sending NIP-17 DM to relay: ${message.substring(0, 50)}...`, 'INFO'); // Step 1: Build unsigned rumor (kind 14) const rumor = { kind: 14, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), // Canonical time for rumor tags: [["p", relayPubkey]], content: message }; // NOTE: Rumor remains unsigned per NIP-59 log('Rumor built (unsigned), creating seal...', 'INFO'); // Step 2: Create seal (kind 13) const seal = { kind: 13, pubkey: userPubkey, created_at: randomNow(), // Randomized to past for metadata protection tags: [], // Empty tags per NIP-59 content: await window.nostr.nip44.encrypt(relayPubkey, JSON.stringify(rumor)) }; // Sign seal with long-term key const signedSeal = await window.nostr.signEvent(seal); if (!signedSeal || !signedSeal.sig) { throw new Error('Failed to sign seal event'); } log('Seal created and signed, creating gift wrap...', 'INFO'); // Step 3: Create gift wrap (kind 1059) with ephemeral key const ephemeralPriv = window.NostrTools.generateSecretKey(); const ephemeralPub = window.NostrTools.getPublicKey(ephemeralPriv); const giftWrap = { kind: 1059, pubkey: ephemeralPub, created_at: randomNow(), // Randomized to past for metadata protection tags: [["p", relayPubkey]], content: await window.NostrTools.nip44.encrypt( JSON.stringify(signedSeal), window.NostrTools.nip44.getConversationKey(ephemeralPriv, relayPubkey) ) }; // Sign gift wrap with ephemeral key using finalizeEvent const signedGiftWrap = window.NostrTools.finalizeEvent(giftWrap, ephemeralPriv); if (!signedGiftWrap || !signedGiftWrap.sig) { throw new Error('Failed to sign gift wrap event'); } log('NIP-17 DM event created and signed with ephemeral key, publishing...', 'INFO'); // Publish via SimplePool const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedGiftWrap); // Use Promise.allSettled to capture per-relay outcomes const results = await Promise.allSettled(publishPromises); // Log detailed publish results let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; log(`✅ NIP-17 DM published successfully to relay ${index}`, 'INFO'); } else { log(`❌ NIP-17 DM failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR'); } }); if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected NIP-17 DM event. Details: ${errorDetails}`); } // Clear the outbox and show success dmOutbox.value = ''; log('NIP-17 DM sent successfully', 'INFO'); // Add to inbox for display addMessageToInbox('sent', message, new Date().toLocaleString()); } catch (error) { log(`Failed to send NIP-17 DM: ${error.message}`, 'ERROR'); } } // Add message to inbox display function addMessageToInbox(direction, message, timestamp) { if (!dmInbox) return; const messageDiv = document.createElement('div'); messageDiv.className = 'log-entry'; const directionColor = direction === 'sent' ? '#007bff' : '#28a745'; // Convert newlines to
tags for proper HTML display const formattedMessage = message.replace(/\n/g, '
'); messageDiv.innerHTML = ` ${timestamp} [${direction.toUpperCase()}] ${formattedMessage} `; // Remove the "No messages received yet" placeholder if it exists const placeholder = dmInbox.querySelector('.log-entry'); if (placeholder && placeholder.textContent === 'No messages received yet.') { dmInbox.innerHTML = ''; } // Add new message at the top dmInbox.insertBefore(messageDiv, dmInbox.firstChild); // Limit to last 50 messages while (dmInbox.children.length > 50) { dmInbox.removeChild(dmInbox.lastChild); } } // Helper function to get relay pubkey function getRelayPubkey() { // Use the dynamically fetched relay pubkey if available if (relayPubkey && isRelayConnected) { return relayPubkey; } // Fallback to hardcoded value for testing/development log('Warning: Using hardcoded relay pubkey. Please connect to relay first.', 'WARNING'); return '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa'; } // Enhanced SimplePool message handler to capture test responses function enhancePoolForTesting() { // SimplePool handles message parsing automatically, so we just need to // ensure our event handlers log appropriately. This is already done // in the subscription onevent callback. console.log('SimplePool enhanced for testing - automatic message handling enabled'); } // Generate random test pubkey function function generateRandomTestKey() { // Generate 32 random bytes (64 hex characters) for a valid pubkey const randomBytes = new Uint8Array(32); crypto.getRandomValues(randomBytes); // Convert to hex string const hexPubkey = Array.from(randomBytes) .map(b => b.toString(16).padStart(2, '0')) .join(''); // Set the generated key in the input field const testPubkeyInput = document.getElementById('test-pubkey-input'); if (testPubkeyInput) { testPubkeyInput.value = hexPubkey; logTestEvent('INFO', `Generated random test pubkey: ${hexPubkey.substring(0, 16)}...`, 'KEYGEN'); } return hexPubkey; } // ================================ // DATABASE STATISTICS FUNCTIONS // ================================ // Send restart command to restart the relay using Administrator API async function sendRestartCommand() { if (!isLoggedIn || !userPubkey) { log('Must be logged in to restart relay', 'ERROR'); return; } if (!relayPool) { log('SimplePool connection not available', 'ERROR'); return; } try { log('Sending restart command to relay...', 'INFO'); // Create command array for restart const command_array = ["system_command", "restart"]; // Encrypt the command array directly using NIP-44 const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); if (!encrypted_content) { throw new Error('Failed to encrypt command array'); } // Create single kind 23456 admin event const restartEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [["p", getRelayPubkey()]], content: encrypted_content }; // Sign the event const signedEvent = await window.nostr.signEvent(restartEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } // Publish via SimplePool const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes const results = await Promise.allSettled(publishPromises); // Check if any relay accepted the event let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; log(`Restart command published successfully to relay ${index}`, 'INFO'); } else { log(`Restart command failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR'); } }); if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected restart command. Details: ${errorDetails}`); } log('Restart command sent successfully - relay should restart shortly...', 'INFO'); // Update connection status to indicate restart is in progress updateRelayConnectionStatus('connecting'); relayConnectionStatus.textContent = 'RESTARTING...'; // The relay will disconnect and need to be reconnected after restart // This will be handled by the WebSocket disconnection event } catch (error) { log(`Failed to send restart command: ${error.message}`, 'ERROR'); updateRelayConnectionStatus('error'); } } // Send stats_query command to get database statistics using Administrator API (inner events) async function sendStatsQuery() { if (!isLoggedIn || !userPubkey) { log('Must be logged in to query database statistics', 'ERROR'); updateStatsStatus('error', 'Not logged in'); return; } if (!relayPool) { log('SimplePool connection not available', 'ERROR'); updateStatsStatus('error', 'No relay connection'); return; } try { updateStatsStatus('loading', 'Querying database...'); // Create command array for stats query const command_array = ["stats_query", "all"]; // Encrypt the command array directly using NIP-44 const encrypted_content = await encryptForRelay(JSON.stringify(command_array)); if (!encrypted_content) { throw new Error('Failed to encrypt command array'); } // Create single kind 23456 admin event const statsEvent = { kind: 23456, pubkey: userPubkey, created_at: Math.floor(Date.now() / 1000), tags: [["p", getRelayPubkey()]], content: encrypted_content }; // Sign the event const signedEvent = await window.nostr.signEvent(statsEvent); if (!signedEvent || !signedEvent.sig) { throw new Error('Event signing failed'); } log('Sending stats query command...', 'INFO'); // Publish via SimplePool const url = relayConnectionUrl.value.trim(); const publishPromises = relayPool.publish([url], signedEvent); // Use Promise.allSettled to capture per-relay outcomes const results = await Promise.allSettled(publishPromises); // Check if any relay accepted the event let successCount = 0; results.forEach((result, index) => { if (result.status === 'fulfilled') { successCount++; log(`Stats query published successfully to relay ${index}`, 'INFO'); } else { log(`Stats query failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR'); } }); if (successCount === 0) { const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; '); throw new Error(`All relays rejected stats query event. Details: ${errorDetails}`); } log('Stats query command sent successfully - waiting for response...', 'INFO'); updateStatsStatus('waiting', 'Waiting for response...'); } catch (error) { log(`Failed to send stats query: ${error.message}`, 'ERROR'); updateStatsStatus('error', error.message); } } // Handle stats_query response and populate tables function handleStatsQueryResponse(responseData) { try { log('Processing stats query response...', 'INFO'); console.log('Stats response data:', responseData); if (responseData.query_type !== 'stats_query') { log('Ignoring non-stats response', 'WARNING'); return; } // Populate overview table populateStatsOverview(responseData); // Populate event kinds table populateStatsKinds(responseData); // Populate time-based statistics populateStatsTime(responseData); // Populate top pubkeys table populateStatsPubkeys(responseData); updateStatsStatus('loaded'); log('Database statistics updated successfully', 'INFO'); } catch (error) { log(`Error processing stats response: ${error.message}`, 'ERROR'); updateStatsStatus('error', 'Failed to process response'); } } // Populate database overview table function populateStatsOverview(data) { if (!data) return; // Update individual cells const dbSize = document.getElementById('db-size'); const totalEvents = document.getElementById('total-events'); const oldestEvent = document.getElementById('oldest-event'); const newestEvent = document.getElementById('newest-event'); if (dbSize) dbSize.textContent = data.database_size_bytes ? formatFileSize(data.database_size_bytes) : '-'; if (totalEvents) totalEvents.textContent = data.total_events || '-'; if (oldestEvent) oldestEvent.textContent = data.database_created_at ? formatTimestamp(data.database_created_at) : '-'; if (newestEvent) newestEvent.textContent = data.latest_event_at ? formatTimestamp(data.latest_event_at) : '-'; } // Populate event kinds distribution table function populateStatsKinds(data) { const tableBody = document.getElementById('stats-kinds-table-body'); if (!tableBody || !data.event_kinds) return; tableBody.innerHTML = ''; if (data.event_kinds.length === 0) { const row = document.createElement('tr'); row.innerHTML = 'No event data'; tableBody.appendChild(row); return; } data.event_kinds.forEach(kind => { const row = document.createElement('tr'); row.innerHTML = ` ${kind.kind} ${kind.count} ${kind.percentage}% `; tableBody.appendChild(row); }); } // Populate time-based statistics table function populateStatsTime(data) { if (!data) return; const events24h = document.getElementById('events-24h'); const events7d = document.getElementById('events-7d'); const events30d = document.getElementById('events-30d'); // Access the nested time_stats object from backend response const timeStats = data.time_stats || {}; if (events24h) events24h.textContent = timeStats.last_24h || '0'; if (events7d) events7d.textContent = timeStats.last_7d || '0'; if (events30d) events30d.textContent = timeStats.last_30d || '0'; } // Populate top pubkeys table function populateStatsPubkeys(data) { const tableBody = document.getElementById('stats-pubkeys-table-body'); if (!tableBody || !data.top_pubkeys) return; tableBody.innerHTML = ''; if (data.top_pubkeys.length === 0) { const row = document.createElement('tr'); row.innerHTML = 'No pubkey data'; tableBody.appendChild(row); return; } data.top_pubkeys.forEach((pubkey, index) => { const row = document.createElement('tr'); const shortPubkey = pubkey.pubkey ? pubkey.pubkey.substring(0, 16) + '...' : '-'; row.innerHTML = ` ${index + 1} ${shortPubkey} ${pubkey.event_count} ${pubkey.percentage}% `; tableBody.appendChild(row); }); } // Update statistics status indicator (disabled - status display removed) function updateStatsStatus(status, message = '') { // Status display has been removed from the UI return; } // Utility function to format file size function formatFileSize(bytes) { if (!bytes || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } // Utility function to format timestamp function formatTimestamp(timestamp) { if (!timestamp) return '-'; const date = new Date(timestamp * 1000); return date.toLocaleString(); } // Event handlers for test buttons document.addEventListener('DOMContentLoaded', () => { // Test button event handlers const testGetAuthRulesBtn = document.getElementById('test-get-auth-rules-btn'); const testClearAuthRulesBtn = document.getElementById('test-clear-auth-rules-btn'); const testAddBlacklistBtn = document.getElementById('test-add-blacklist-btn'); const testAddWhitelistBtn = document.getElementById('test-add-whitelist-btn'); const testConfigQueryBtn = document.getElementById('test-config-query-btn'); const testPostEventBtn = document.getElementById('test-post-event-btn'); const clearTestLogBtn = document.getElementById('clear-test-log-btn'); const generateTestKeyBtn = document.getElementById('generate-test-key-btn'); if (testGetAuthRulesBtn) { testGetAuthRulesBtn.addEventListener('click', testGetAuthRules); } if (testClearAuthRulesBtn) { testClearAuthRulesBtn.addEventListener('click', testClearAuthRules); } if (testAddBlacklistBtn) { testAddBlacklistBtn.addEventListener('click', testAddBlacklist); } if (testAddWhitelistBtn) { testAddWhitelistBtn.addEventListener('click', testAddWhitelist); } if (testConfigQueryBtn) { testConfigQueryBtn.addEventListener('click', testConfigQuery); } if (testPostEventBtn) { testPostEventBtn.addEventListener('click', testPostEvent); } if (clearTestLogBtn) { clearTestLogBtn.addEventListener('click', () => { const testLog = document.getElementById('test-event-log'); if (testLog) { testLog.innerHTML = '
SYSTEM: Test log cleared.
'; } }); } if (generateTestKeyBtn) { generateTestKeyBtn.addEventListener('click', generateRandomTestKey); } // Show test input section when needed const testInputSection = document.getElementById('test-input-section'); if (testInputSection) { testInputSection.style.display = 'block'; } // Database statistics event handlers const refreshStatsBtn = document.getElementById('refresh-stats-btn'); if (refreshStatsBtn) { refreshStatsBtn.addEventListener('click', sendStatsQuery); } // NIP-17 DM event handlers if (sendDmBtn) { sendDmBtn.addEventListener('click', sendNIP17DM); } }); // Initialize the app document.addEventListener('DOMContentLoaded', () => { console.log('C-Relay Admin API interface loaded'); // Initialize login/logout button state updateLoginLogoutButton(); setTimeout(() => { initializeApp(); // Enhance SimplePool for testing after initialization setTimeout(enhancePoolForTesting, 2000); }, 100); });