// 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;
// Database statistics auto-refresh
let statsAutoRefreshInterval = null;
let countdownInterval = null;
let countdownSeconds = 10;
// DOM elements
const loginModal = document.getElementById('login-modal');
const loginModalContainer = document.getElementById('login-modal-container');
const profileArea = document.getElementById('profile-area');
const headerUserImage = document.getElementById('header-user-image');
const headerUserName = document.getElementById('header-user-name');
const logoutDropdown = document.getElementById('logout-dropdown');
const logoutBtn = document.getElementById('logout-btn');
// Legacy elements (kept for backward compatibility)
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
}
// Show authentication warning message
function showAuthenticationWarning(message) {
// Remove any existing warning
hideAuthenticationWarning();
// Create warning element
const warningDiv = document.createElement('div');
warningDiv.id = 'auth-warning-message';
warningDiv.className = 'auth-warning-message';
warningDiv.innerHTML = `
⚠️ Authentication Issue: ${message}
This usually means your pubkey is not authorized as an admin for this relay.
Please check that you are using the correct admin pubkey that was shown during relay startup.
`;
// Insert warning at the top of the relay connection section
const relaySection = document.getElementById('relay-connection-section');
if (relaySection) {
relaySection.insertBefore(warningDiv, relaySection.firstChild);
}
log(`Authentication warning displayed: ${message}`, 'WARNING');
}
// Hide authentication warning message
function hideAuthenticationWarning() {
const warningDiv = document.getElementById('auth-warning-message');
if (warningDiv) {
warningDiv.remove();
}
}
// 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');
// Clear any previous authentication warnings
hideAuthenticationWarning();
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');
// Check if this is an authentication-related error
if (error.message.includes('authentication') ||
error.message.includes('auth') ||
error.message.includes('permission') ||
error.message.includes('unauthorized') ||
error.message.includes('forbidden')) {
updateRelayConnectionStatus('auth_error');
showAuthenticationWarning(error.message);
} else {
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();
// Hide any authentication warnings
hideAuthenticationWarning();
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;
case 'auth_error':
relayConnectionStatus.textContent = 'AUTHENTICATION 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 = 3;
const delay = 200; // ms between attempts (reduced from 500ms)
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 and profile in header
showProfileInHeader();
loadUserProfile();
// Automatically attempt to connect to relay after restoring authentication state
console.log('✅ Authentication state restored - automatically attempting to connect to relay...');
setTimeout(() => {
connectToRelay().catch(error => {
console.log(`Automatic relay connection failed: ${error.message}`);
// Check if this is an authentication-related error
if (error.message.includes('authentication') ||
error.message.includes('auth') ||
error.message.includes('permission') ||
error.message.includes('unauthorized') ||
error.message.includes('forbidden')) {
showAuthenticationWarning(error.message);
}
});
}, 500); // Small delay to allow profile loading to complete
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
}
});
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, showing profile in header');
showProfileInHeader();
} else {
console.log('No existing authentication found, showing login modal');
showLoginModal();
}
// 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}`);
// Hide login modal and show profile in header
hideLoginModal();
showProfileInHeader();
loadUserProfile();
// Automatically attempt to connect to relay after successful login
console.log('Login successful. Automatically attempting to connect to relay...');
setTimeout(() => {
connectToRelay().catch(error => {
console.log(`Automatic relay connection failed: ${error.message}`);
// Check if this is an authentication-related error
if (error.message.includes('authentication') ||
error.message.includes('auth') ||
error.message.includes('permission') ||
error.message.includes('unauthorized')) {
showAuthenticationWarning(error.message);
}
});
}, 500); // Small delay to allow profile loading to complete
} 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();
// Hide any authentication warnings
hideAuthenticationWarning();
// Reset UI - hide profile and show login modal
hideProfileFromHeader();
showLoginModal();
updateConfigStatus(false);
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 databaseStatisticsSection = document.getElementById('databaseStatisticsSection');
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 (databaseStatisticsSection) databaseStatisticsSection.style.display = shouldShow ? 'block' : 'none';
if (nip17DMSection) nip17DMSection.style.display = shouldShow ? 'block' : 'none';
// Start/stop auto-refresh based on visibility
if (shouldShow && databaseStatisticsSection && databaseStatisticsSection.style.display === 'block') {
startStatsAutoRefresh();
} else {
stopStatsAutoRefresh();
}
// Update countdown display when visibility changes
updateCountdownDisplay();
}
// Show login modal
function showLoginModal() {
if (loginModal && loginModalContainer) {
// Initialize the login UI in the modal
if (window.NOSTR_LOGIN_LITE && typeof window.NOSTR_LOGIN_LITE.embed === 'function') {
window.NOSTR_LOGIN_LITE.embed('#login-modal-container', {
seamless: true
});
}
loginModal.style.display = 'flex';
}
}
// Hide login modal
function hideLoginModal() {
if (loginModal) {
loginModal.style.display = 'none';
}
}
// Show profile in header
function showProfileInHeader() {
if (profileArea) {
profileArea.style.display = 'flex';
}
}
// Hide profile from header
function hideProfileFromHeader() {
if (profileArea) {
profileArea.style.display = 'none';
}
// Also hide logout dropdown if visible
if (logoutDropdown) {
logoutDropdown.style.display = 'none';
}
}
// Update login/logout UI visibility (legacy function - kept for backward compatibility)
function updateLoginLogoutUI() {
// This function is now handled by showProfileInHeader() and hideProfileFromHeader()
// Kept for backward compatibility with any existing code that might call it
}
// Show main interface after login (legacy function - kept for backward compatibility)
function showMainInterface() {
// This function is now handled by showProfileInHeader() and updateAdminSectionsVisibility()
// Kept for backward compatibility with any existing code that might call it
updateAdminSectionsVisibility();
}
// Load user profile using nostr-tools pool
async function loadUserProfile() {
if (!userPubkey) return;
console.log('Loading user profile...');
// Update header display (new system)
if (headerUserName) {
headerUserName.textContent = 'Loading...';
}
// Update legacy elements if they exist (backward compatibility)
if (persistentUserName) {
persistentUserName.textContent = 'Loading...';
}
if (persistentUserAbout) {
persistentUserAbout.textContent = 'Loading...';
}
// Convert hex pubkey to npub for initial display
let displayPubkey = userPubkey;
let npubLink = '';
try {
if (userPubkey && userPubkey.length === 64 && /^[0-9a-fA-F]+$/.test(userPubkey)) {
const npub = window.NostrTools.nip19.npubEncode(userPubkey);
displayPubkey = npub;
npubLink = `${npub}`;
}
} catch (error) {
console.log('Failed to encode user pubkey to npub:', error.message);
}
if (persistentUserPubkey) {
if (npubLink) {
persistentUserPubkey.innerHTML = npubLink;
} else {
persistentUserPubkey.textContent = displayPubkey;
}
}
try {
// Create a SimplePool instance for profile loading
const profilePool = new window.NostrTools.SimplePool();
const relays = ['wss://relay.damus.io',
'wss://relay.nostr.band',
'wss://nos.lol',
'wss://relay.primal.net',
'wss://relay.snort.social',
'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);
// Update header display (new system)
if (headerUserName) {
headerUserName.textContent = 'Anonymous User';
}
// Update legacy elements if they exist (backward compatibility)
if (persistentUserName) {
persistentUserName.textContent = 'Anonymous User';
}
if (persistentUserAbout) {
persistentUserAbout.textContent = 'No profile found';
}
// Keep the npub display
}
// Close the profile pool
profilePool.close(relays);
} catch (error) {
console.log('Profile loading failed: ' + error.message);
// Update header display (new system)
if (headerUserName) {
headerUserName.textContent = 'Error loading profile';
}
// Update legacy elements if they exist (backward compatibility)
if (persistentUserName) {
persistentUserName.textContent = 'Error loading profile';
}
if (persistentUserAbout) {
persistentUserAbout.textContent = error.message;
}
// Keep the npub display
}
}
// Display profile data
function displayProfile(profile) {
const name = profile.name || profile.display_name || profile.displayName || 'Anonymous User';
const about = profile.about || 'No description provided';
const picture = profile.picture || profile.image || null;
// Convert hex pubkey to npub for display
let displayPubkey = userPubkey;
let npubLink = '';
try {
if (userPubkey && userPubkey.length === 64 && /^[0-9a-fA-F]+$/.test(userPubkey)) {
const npub = window.NostrTools.nip19.npubEncode(userPubkey);
displayPubkey = npub;
npubLink = `${npub}`;
}
} catch (error) {
console.log('Failed to encode user pubkey to npub:', error.message);
}
// Update header profile display
if (headerUserName) {
headerUserName.textContent = name;
}
// Handle header profile picture
if (headerUserImage) {
if (picture && typeof picture === 'string' && (picture.startsWith('http') || picture.startsWith('https'))) {
headerUserImage.src = picture;
headerUserImage.style.display = 'block';
headerUserImage.onerror = function() {
// Hide image on error
this.style.display = 'none';
console.log('Profile image failed to load:', picture);
};
} else {
headerUserImage.style.display = 'none';
}
}
// Update legacy persistent user details (kept for backward compatibility)
if (persistentUserName) persistentUserName.textContent = name;
if (persistentUserPubkey && npubLink) {
persistentUserPubkey.innerHTML = npubLink;
} else if (persistentUserPubkey) {
persistentUserPubkey.textContent = displayPubkey;
}
if (persistentUserAbout) persistentUserAbout.textContent = about;
// Handle legacy profile picture
const userImageContainer = document.getElementById('persistent-user-image');
if (userImageContainer) {
if (picture && typeof picture === 'string' && picture.startsWith('http')) {
// Create or update image element
let img = userImageContainer.querySelector('img');
if (!img) {
img = document.createElement('img');
img.className = 'user-profile-image';
img.alt = `${name}'s profile picture`;
img.onerror = function() {
// Hide image on error
this.style.display = 'none';
};
userImageContainer.appendChild(img);
}
img.src = picture;
img.style.display = 'block';
} else {
// Hide image if no valid picture
const img = userImageContainer.querySelector('img');
if (img) {
img.style.display = 'none';
}
}
}
console.log(`Profile loaded for: ${name} with pubkey: ${userPubkey}`);
}
// Logout function
async function logout() {
log('Logging out...', 'INFO');
try {
// Stop auto-refresh before disconnecting
stopStatsAutoRefresh();
// Clean up relay connection
disconnectFromRelay();
// Clean up configuration pool
if (relayPool) {
log('Closing configuration pool...', 'INFO');
const url = relayConnectionUrl.value.trim();
if (url) {
relayPool.close([url]);
}
relayPool = null;
subscriptionId = null;
}
await nlLite.logout();
userPubkey = null;
isLoggedIn = false;
currentConfig = null;
// Hide any authentication warnings
hideAuthenticationWarning();
// Reset UI - hide profile and show login modal
hideProfileFromHeader();
// showLoginModal() removed - handled by handleLogoutEvent()
updateConfigStatus(false);
updateAdminSectionsVisibility();
log('Logged out successfully', 'INFO');
} catch (error) {
log('Logout failed: ' + error.message, 'ERROR');
}
}
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, event.pubkey);
// 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, rumor.pubkey);
// 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;
}
}
// Profile area click handler for logout dropdown
function toggleLogoutDropdown(event) {
if (!logoutDropdown) return;
// Only toggle if clicking on the image, not the text or container
if (event.target === headerUserImage) {
const isVisible = logoutDropdown.style.display === 'block';
logoutDropdown.style.display = isVisible ? 'none' : 'block';
}
}
// Close logout dropdown when clicking outside
document.addEventListener('click', function(event) {
if (profileArea && logoutDropdown && !profileArea.contains(event.target)) {
logoutDropdown.style.display = 'none';
}
});
// Initialize profile area click handler
if (profileArea) {
profileArea.addEventListener('click', toggleLogoutDropdown);
}
// Initialize logout button handler
if (logoutBtn) {
logoutBtn.addEventListener('click', function(e) {
e.stopPropagation(); // Prevent profile area click
logout();
});
}
// Event handlers
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 = `
`;
tableBody.appendChild(row);
});
}
// Populate time-based statistics table
function populateStatsTime(data) {
if (!data) return;
// Access the nested time_stats object from backend response
const timeStats = data.time_stats || {};
// Update cells with flash animation for changed values
updateStatsCell('events-24h', timeStats.last_24h || '0');
updateStatsCell('events-7d', timeStats.last_7d || '0');
updateStatsCell('events-30d', 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');
// Convert hex pubkey to npub for display
let displayPubkey = pubkey.pubkey || '-';
let npubLink = displayPubkey;
try {
if (pubkey.pubkey && pubkey.pubkey.length === 64 && /^[0-9a-fA-F]+$/.test(pubkey.pubkey)) {
const npub = window.NostrTools.nip19.npubEncode(pubkey.pubkey);
displayPubkey = npub;
npubLink = `${npub}`;
}
} catch (error) {
console.log('Failed to encode pubkey to npub:', error.message);
}
row.innerHTML = `
${index + 1}
${npubLink}
${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();
}
// Update statistics cell with flash animation if value changed
function updateStatsCell(cellId, newValue) {
const cell = document.getElementById(cellId);
if (!cell) return;
const currentValue = cell.textContent;
cell.textContent = newValue;
// Flash if value changed
if (currentValue !== newValue && currentValue !== '-') {
cell.classList.add('flash-value');
setTimeout(() => {
cell.classList.remove('flash-value');
}, 500);
}
}
// Start auto-refreshing database statistics every 10 seconds
function startStatsAutoRefresh() {
// Clear any existing interval
stopStatsAutoRefresh();
// Reset countdown
countdownSeconds = 10;
updateCountdownDisplay();
// Start countdown interval - update every second
countdownInterval = setInterval(() => {
countdownSeconds--;
updateCountdownDisplay();
if (countdownSeconds <= 0) {
// Time to refresh
if (isLoggedIn && isRelayConnected) {
log('Auto-refreshing database statistics...', 'INFO');
sendStatsQuery().then(() => {
// Flash button red on successful refresh
flashRefreshButton();
// Reset countdown
countdownSeconds = 10;
updateCountdownDisplay();
}).catch(error => {
log(`Auto-refresh failed: ${error.message}`, 'ERROR');
// Reset countdown even on failure
countdownSeconds = 10;
updateCountdownDisplay();
});
} else {
// Reset countdown if not logged in/connected
countdownSeconds = 10;
updateCountdownDisplay();
}
}
}, 1000); // Update every 1 second
log('Database statistics auto-refresh started (10 second intervals)', 'INFO');
}
// Stop auto-refreshing database statistics
function stopStatsAutoRefresh() {
if (statsAutoRefreshInterval) {
clearInterval(statsAutoRefreshInterval);
statsAutoRefreshInterval = null;
}
if (countdownInterval) {
clearInterval(countdownInterval);
countdownInterval = null;
}
// Reset countdown display
updateCountdownDisplay();
log('Database statistics auto-refresh stopped', 'INFO');
}
// Update countdown display in refresh button
function updateCountdownDisplay() {
const refreshBtn = document.getElementById('refresh-stats-btn');
if (!refreshBtn) return;
if (countdownInterval && isLoggedIn && isRelayConnected) {
// Japanese Kanji numbers: 〇 一 二 三 四 五 六 七 八 九 十
const kanjiNumbers = ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
// Show single character counting down from 十 (10) to 〇 (0)
if (countdownSeconds >= 0 && countdownSeconds <= 10) {
refreshBtn.textContent = kanjiNumbers[countdownSeconds];
} else {
refreshBtn.textContent = '';
}
} else {
// Show empty when not active
refreshBtn.textContent = '';
}
}
// Flash refresh button red on successful refresh
function flashRefreshButton() {
const refreshBtn = document.getElementById('refresh-stats-btn');
if (!refreshBtn) return;
// Add red flash class
refreshBtn.classList.add('flash-red');
// Remove flash class after animation
setTimeout(() => {
refreshBtn.classList.remove('flash-red');
}, 500); // Match CSS animation duration
}
// 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);
}
});
// Set default relay URL based on where the page is being served from
function setDefaultRelayUrl() {
const relayUrlInput = document.getElementById('relay-connection-url');
if (!relayUrlInput) return;
// Get the current page's protocol and hostname
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const hostname = window.location.hostname;
const port = window.location.port;
// Construct the relay URL
let relayUrl;
if (hostname === 'localhost' || hostname === '127.0.0.1') {
// For localhost, default to ws://localhost:8888
relayUrl = 'ws://localhost:8888';
} else {
// For production, use the same hostname with WebSocket protocol
// Remove port from URL since relay typically runs on standard ports (80/443)
relayUrl = `${protocol}//${hostname}`;
}
relayUrlInput.value = relayUrl;
log(`Default relay URL set to: ${relayUrl}`, 'INFO');
}
// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
console.log('C-Relay Admin API interface loaded');
// Set default relay URL based on current page location
setDefaultRelayUrl();
// Ensure admin sections are hidden by default on page load
updateAdminSectionsVisibility();
setTimeout(() => {
initializeApp();
// Enhance SimplePool for testing after initialization
setTimeout(enhancePoolForTesting, 2000);
}, 100);
});