Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0c0754e83 | ||
|
|
3265e3d114 | ||
|
|
927659ece1 | ||
|
|
b6ff4150b4 | ||
|
|
63bc526163 | ||
|
|
a1f712236a | ||
|
|
06e6c17b7b |
+8
-4
@@ -112,6 +112,10 @@
|
||||
<td>Process ID</td>
|
||||
<td id="process-id">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WebSocket Connections</td>
|
||||
<td id="websocket-connections">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Active Subscriptions</td>
|
||||
<td id="active-subscriptions">-</td>
|
||||
@@ -120,14 +124,14 @@
|
||||
<td>Memory Usage</td>
|
||||
<td id="memory-usage">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPU Core</td>
|
||||
<td id="cpu-core">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPU Usage</td>
|
||||
<td id="cpu-usage">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPU Core</td>
|
||||
<td id="cpu-core">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Oldest Event</td>
|
||||
<td id="oldest-event">-</td>
|
||||
|
||||
+339
-114
@@ -51,6 +51,12 @@ let pendingSqlQueries = new Map();
|
||||
let eventRateChart = null;
|
||||
let previousTotalEvents = 0; // Track previous total for rate calculation
|
||||
|
||||
// Temporary diagnostics toggle: disable external profile relay lookups
|
||||
const DISABLE_EXTERNAL_PROFILE_RELAYS = false;
|
||||
|
||||
// Temporary diagnostics toggle: enable deep SimplePool diagnostics in nostr.bundle.js
|
||||
window.__SIMPLE_POOL_DEBUG__ = true;
|
||||
|
||||
// Relay Events state - now handled by main subscription
|
||||
|
||||
// DOM elements
|
||||
@@ -85,6 +91,47 @@ function log(message, type = 'INFO') {
|
||||
// UI logging removed - using console only
|
||||
}
|
||||
|
||||
function normalizeRelayUrlForPool(url) {
|
||||
try {
|
||||
let normalized = url || '';
|
||||
if (!normalized.includes('://')) {
|
||||
normalized = 'wss://' + normalized;
|
||||
}
|
||||
const parsed = new URL(normalized);
|
||||
parsed.pathname = parsed.pathname.replace(/\/+/g, '/');
|
||||
if (parsed.pathname.endsWith('/')) {
|
||||
parsed.pathname = parsed.pathname.slice(0, -1);
|
||||
}
|
||||
if ((parsed.port === '80' && parsed.protocol === 'ws:') ||
|
||||
(parsed.port === '443' && parsed.protocol === 'wss:')) {
|
||||
parsed.port = '';
|
||||
}
|
||||
parsed.searchParams.sort();
|
||||
parsed.hash = '';
|
||||
return parsed.toString();
|
||||
} catch (error) {
|
||||
console.warn('Failed to normalize relay URL for diagnostics:', url, error?.message || error);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNip42Auth(url, challenge, context = 'SimplePool') {
|
||||
console.log(`🔐 NIP-42 AUTH challenge during ${context}:`, challenge);
|
||||
try {
|
||||
if (!window.nostr) {
|
||||
console.error(`NIP-42 auth required during ${context} but no nostr extension available`);
|
||||
return;
|
||||
}
|
||||
|
||||
const authEvent = window.NostrTools.nip42.makeAuthEvent(url, challenge);
|
||||
const signedAuthEvent = await window.nostr.signEvent(authEvent);
|
||||
console.log(`✅ NIP-42 AUTH signed for ${context}`);
|
||||
return signedAuthEvent;
|
||||
} catch (authError) {
|
||||
console.error(`NIP-42 AUTH failed during ${context}:`, authError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function log(message, type = 'INFO') {
|
||||
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
|
||||
@@ -237,9 +284,38 @@ async function checkExistingAuthWithRetries() {
|
||||
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;
|
||||
// Extension auth must be actively restorable, otherwise treat as stale and continue.
|
||||
if (authState.method === 'extension') {
|
||||
console.log('Stored extension auth found, verifying live extension availability...');
|
||||
let restoredExtensionAuth = null;
|
||||
|
||||
if (window.nostr && typeof window.nostr.restoreAuthState === 'function') {
|
||||
restoredExtensionAuth = await window.nostr.restoreAuthState();
|
||||
}
|
||||
|
||||
if (restoredExtensionAuth && restoredExtensionAuth.pubkey) {
|
||||
console.log('✅ Extension auth restored successfully:', restoredExtensionAuth.pubkey);
|
||||
console.log('✅ LOGIN ATTEMPT RESULT: SUCCESS (restored extension session)', {
|
||||
method: 'extension',
|
||||
pubkey: restoredExtensionAuth.pubkey
|
||||
});
|
||||
await restoreAuthenticationState(restoredExtensionAuth.pubkey);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('⚠️ Stored extension auth is stale (no live extension), clearing and continuing');
|
||||
if (typeof window.NOSTR_LOGIN_LITE.clearAuthState === 'function') {
|
||||
window.NOSTR_LOGIN_LITE.clearAuthState();
|
||||
}
|
||||
} else {
|
||||
console.log('✅ Auth state found via NOSTR_LOGIN_LITE.getAuthState():', authState.pubkey);
|
||||
console.log('✅ LOGIN ATTEMPT RESULT: SUCCESS (restored NOSTR_LOGIN_LITE auth state)', {
|
||||
method: authState.method || 'unknown',
|
||||
pubkey: authState.pubkey
|
||||
});
|
||||
await restoreAuthenticationState(authState.pubkey);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +325,10 @@ async function checkExistingAuthWithRetries() {
|
||||
const pubkey = await nlLite.getPublicKey();
|
||||
if (pubkey && pubkey.length === 64) {
|
||||
console.log('✅ Pubkey found via nlLite.getPublicKey():', pubkey);
|
||||
console.log('✅ LOGIN ATTEMPT RESULT: SUCCESS (restored nlLite session)', {
|
||||
method: 'restored-nlLite',
|
||||
pubkey
|
||||
});
|
||||
await restoreAuthenticationState(pubkey);
|
||||
return true;
|
||||
}
|
||||
@@ -260,6 +340,10 @@ async function checkExistingAuthWithRetries() {
|
||||
const pubkey = await window.nostr.getPublicKey();
|
||||
if (pubkey && pubkey.length === 64) {
|
||||
console.log('✅ Pubkey found via window.nostr.getPublicKey():', pubkey);
|
||||
console.log('✅ LOGIN ATTEMPT RESULT: SUCCESS (restored window.nostr session)', {
|
||||
method: 'restored-window.nostr',
|
||||
pubkey
|
||||
});
|
||||
await restoreAuthenticationState(pubkey);
|
||||
return true;
|
||||
}
|
||||
@@ -272,6 +356,10 @@ async function checkExistingAuthWithRetries() {
|
||||
const parsedData = JSON.parse(localStorageData);
|
||||
if (parsedData.pubkey) {
|
||||
console.log('✅ Pubkey found in localStorage:', parsedData.pubkey);
|
||||
console.log('✅ LOGIN ATTEMPT RESULT: SUCCESS (restored localStorage session)', {
|
||||
method: 'restored-localStorage',
|
||||
pubkey: parsedData.pubkey
|
||||
});
|
||||
await restoreAuthenticationState(parsedData.pubkey);
|
||||
return true;
|
||||
}
|
||||
@@ -295,6 +383,7 @@ async function checkExistingAuthWithRetries() {
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('❌ LOGIN ATTEMPT RESULT: NO STORED SESSION FOUND (showing login modal)');
|
||||
console.log('🔍 Authentication detection completed - no existing auth found after all attempts');
|
||||
return false;
|
||||
}
|
||||
@@ -314,7 +403,12 @@ async function restoreAuthenticationState(pubkey) {
|
||||
await setupAutomaticRelayConnection(false);
|
||||
|
||||
// Verify admin access before showing admin sections
|
||||
verifyAdminAccess();
|
||||
const adminCheckResult = await verifyAdminAccess('restore-auth-state');
|
||||
if (adminCheckResult?.passed) {
|
||||
console.log('✅ POST-LOGIN ADMIN CHECK PASSED (restore):', adminCheckResult.reason);
|
||||
} else {
|
||||
console.warn('❌ POST-LOGIN ADMIN CHECK FAILED (restore):', adminCheckResult?.reason || 'Unknown reason');
|
||||
}
|
||||
|
||||
console.log('✅ Authentication state restored successfully');
|
||||
}
|
||||
@@ -447,7 +541,7 @@ async function initializeApp() {
|
||||
}
|
||||
|
||||
// Handle authentication events
|
||||
function handleAuthEvent(event) {
|
||||
async function handleAuthEvent(event) {
|
||||
const { pubkey, method, error } = event.detail;
|
||||
|
||||
if (method && pubkey) {
|
||||
@@ -455,20 +549,34 @@ function handleAuthEvent(event) {
|
||||
isLoggedIn = true;
|
||||
console.log(`Login successful! Method: ${method}`);
|
||||
console.log(`Public key: ${pubkey}`);
|
||||
console.log('✅ LOGIN ATTEMPT RESULT: SUCCESS (interactive login)', { method, pubkey });
|
||||
|
||||
// Hide login modal and show profile in header
|
||||
hideLoginModal();
|
||||
showProfileInHeader();
|
||||
loadUserProfile();
|
||||
|
||||
console.log('🔎 POST-LOGIN DEBUG: Starting admin verification flow', {
|
||||
method,
|
||||
pubkey,
|
||||
isLoggedIn,
|
||||
isRelayConnected
|
||||
});
|
||||
|
||||
// Automatically set up relay connection
|
||||
setupAutomaticRelayConnection(false); // Don't show sections yet - wait for admin verification
|
||||
await setupAutomaticRelayConnection(false); // Don't show sections yet - wait for admin verification
|
||||
|
||||
// Verify admin access before showing admin sections
|
||||
verifyAdminAccess();
|
||||
const adminCheckResult = await verifyAdminAccess('auth-event');
|
||||
if (adminCheckResult?.passed) {
|
||||
console.log('✅ POST-LOGIN ADMIN CHECK PASSED:', adminCheckResult.reason);
|
||||
} else {
|
||||
console.warn('❌ POST-LOGIN ADMIN CHECK FAILED:', adminCheckResult?.reason || 'Unknown reason');
|
||||
}
|
||||
|
||||
} else if (error) {
|
||||
console.log(`Authentication error: ${error}`);
|
||||
console.warn('❌ LOGIN ATTEMPT RESULT: FAILED (interactive login)', { error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,22 +617,40 @@ function handleLogoutEvent() {
|
||||
}
|
||||
|
||||
// Verify admin access by sending a system_status command and waiting for response
|
||||
async function verifyAdminAccess() {
|
||||
console.log('=== VERIFYING ADMIN ACCESS (BYPASS MODE) ===');
|
||||
async function verifyAdminAccess(source = 'unknown') {
|
||||
console.log('=== VERIFYING ADMIN ACCESS (BYPASS MODE) ===', { source, isLoggedIn, isRelayConnected });
|
||||
|
||||
if (!isLoggedIn || !isRelayConnected) {
|
||||
console.log('Cannot verify admin access - not logged in or not connected to relay');
|
||||
showAccessDeniedOverlay();
|
||||
return;
|
||||
if (!isLoggedIn) {
|
||||
const reason = 'User is not logged in';
|
||||
console.warn('❌ Admin check failed:', reason, { source });
|
||||
isAdminVerified = false;
|
||||
pendingAdminVerification = false;
|
||||
hideAccessDeniedOverlay();
|
||||
updateAdminSectionsVisibility();
|
||||
return { passed: false, reason, source };
|
||||
}
|
||||
|
||||
if (!isRelayConnected) {
|
||||
const reason = 'Relay connection is not ready yet';
|
||||
console.warn('⚠️ Admin check deferred:', reason, { source });
|
||||
pendingAdminVerification = false;
|
||||
isAdminVerified = false;
|
||||
hideAccessDeniedOverlay(); // Do not show ACCESS DENIED while connection is still establishing
|
||||
updateAdminSectionsVisibility();
|
||||
return { passed: false, reason, source, deferred: true };
|
||||
}
|
||||
|
||||
// TEMPORARY BYPASS: Skip admin verification and grant access immediately
|
||||
// TODO: Re-enable once idle_connection_timeout_sec is increased to 120+
|
||||
pendingAdminVerification = false;
|
||||
isAdminVerified = true;
|
||||
hideAccessDeniedOverlay();
|
||||
hideAdminVerificationLoading();
|
||||
updateAdminSectionsVisibility();
|
||||
console.log('✅ Admin verification bypassed (temporary)');
|
||||
|
||||
const reason = 'Bypass mode enabled (temporary)';
|
||||
console.log('✅ Admin verification passed:', reason, { source });
|
||||
return { passed: true, reason, source, bypass: true };
|
||||
}
|
||||
|
||||
// Show loading indicator while verifying admin access
|
||||
@@ -749,6 +875,20 @@ async function loadUserProfile() {
|
||||
}
|
||||
|
||||
try {
|
||||
if (DISABLE_EXTERNAL_PROFILE_RELAYS) {
|
||||
console.warn('🧪 PROFILE RELAY TEST: External profile relays disabled; skipping profile query');
|
||||
if (headerUserName) {
|
||||
headerUserName.textContent = 'External profile lookup disabled (test mode)';
|
||||
}
|
||||
if (persistentUserName) {
|
||||
persistentUserName.textContent = 'External profile lookup disabled (test mode)';
|
||||
}
|
||||
if (persistentUserAbout) {
|
||||
persistentUserAbout.textContent = 'No external relay query performed';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a SimplePool instance for profile loading
|
||||
const profilePool = new window.NostrTools.SimplePool();
|
||||
const relays = ['wss://relay.damus.io',
|
||||
@@ -975,40 +1115,17 @@ function attachWebSocketMonitoring(relayPool, url) {
|
||||
|
||||
// SimplePool stores connections in _conn object
|
||||
if (relayPool && relayPool._conn) {
|
||||
// Monitor when connections are created
|
||||
const originalGetConnection = relayPool._conn[url];
|
||||
if (originalGetConnection) {
|
||||
// Diagnostics only: inspect existing connection without mutating _conn internals
|
||||
const existingConn = relayPool._conn[url];
|
||||
if (existingConn) {
|
||||
console.log('📡 Found existing connection for URL:', url);
|
||||
|
||||
// Try to access the WebSocket if it's available
|
||||
const conn = relayPool._conn[url];
|
||||
if (conn && conn.ws) {
|
||||
attachWebSocketEventListeners(conn.ws, url);
|
||||
if (existingConn.ws && !existingConn.ws._monitored) {
|
||||
attachWebSocketEventListeners(existingConn.ws, url);
|
||||
existingConn.ws._monitored = true;
|
||||
}
|
||||
} else {
|
||||
console.log('📡 No existing _conn entry yet for URL:', url);
|
||||
}
|
||||
|
||||
// Override the connection getter to monitor new connections
|
||||
const originalConn = relayPool._conn;
|
||||
relayPool._conn = new Proxy(originalConn, {
|
||||
get(target, prop) {
|
||||
const conn = target[prop];
|
||||
if (conn && conn.ws && !conn.ws._monitored) {
|
||||
console.log('🔗 New WebSocket connection detected for:', prop);
|
||||
attachWebSocketEventListeners(conn.ws, prop);
|
||||
conn.ws._monitored = true;
|
||||
}
|
||||
return conn;
|
||||
},
|
||||
set(target, prop, value) {
|
||||
if (value && value.ws && !value.ws._monitored) {
|
||||
console.log('🔗 WebSocket connection being set for:', prop);
|
||||
attachWebSocketEventListeners(value.ws, prop);
|
||||
value.ws._monitored = true;
|
||||
}
|
||||
target[prop] = value;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ WebSocket monitoring attached');
|
||||
@@ -1160,7 +1277,32 @@ async function subscribeToConfiguration() {
|
||||
console.log(`📝 Generated subscription ID: ${subscriptionId}`);
|
||||
console.log(`👤 User pubkey: ${userPubkey}`);
|
||||
console.log(`🎯 About to call relayPool.subscribeMany with URL: ${url}`);
|
||||
const normalizedUrl = normalizeRelayUrlForPool(url);
|
||||
const relayMapKeysBefore = relayPool?.relays ? Array.from(relayPool.relays.keys()) : [];
|
||||
console.log(`📊 relayPool._conn before subscribeMany:`, Object.keys(relayPool._conn || {}));
|
||||
console.log('🔎 ADMIN SUB DIAG: URL normalization', {
|
||||
rawUrl: url,
|
||||
normalizedUrl,
|
||||
relayMapKeysBefore
|
||||
});
|
||||
const preSubConn = relayPool._conn?.[url] || relayPool._conn?.[normalizedUrl];
|
||||
const preSubRelay = relayPool?.relays?.get?.(normalizedUrl);
|
||||
console.log('🔎 ADMIN SUB DIAG: pre-subscribe connection snapshot', {
|
||||
url,
|
||||
normalizedUrl,
|
||||
hasConn: !!preSubConn,
|
||||
connType: preSubConn?.constructor?.name,
|
||||
hasWs: !!preSubConn?.ws,
|
||||
wsReadyState: preSubConn?.ws?.readyState,
|
||||
wsUrl: preSubConn?.ws?.url,
|
||||
wsProtocol: preSubConn?.ws?.protocol,
|
||||
hasRelayObj: !!preSubRelay,
|
||||
relayConnected: preSubRelay?.connected,
|
||||
isLoggedIn,
|
||||
isRelayConnected,
|
||||
isSubscribed,
|
||||
isSubscribing
|
||||
});
|
||||
|
||||
// Mark as subscribed BEFORE calling subscribeMany to prevent race conditions
|
||||
isSubscribed = true;
|
||||
@@ -1310,42 +1452,38 @@ async function subscribeToConfiguration() {
|
||||
}
|
||||
},
|
||||
async onauth(challenge) {
|
||||
// NIP-42 authentication: relay sent AUTH challenge, sign and respond
|
||||
console.log('🔐 NIP-42 AUTH challenge received:', challenge);
|
||||
try {
|
||||
if (!window.nostr) {
|
||||
console.error('NIP-42 auth required but no nostr extension available');
|
||||
return;
|
||||
}
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const authEvent = window.NostrTools.nip42.makeAuthEvent(url, challenge);
|
||||
const signedAuthEvent = await window.nostr.signEvent(authEvent);
|
||||
console.log('✅ NIP-42 AUTH event signed, responding to challenge');
|
||||
return signedAuthEvent;
|
||||
} catch (authError) {
|
||||
console.error('NIP-42 AUTH failed:', authError.message);
|
||||
}
|
||||
const authUrl = relayConnectionUrl.value.trim();
|
||||
return handleNip42Auth(authUrl, challenge, 'subscription');
|
||||
},
|
||||
onclose(reason) {
|
||||
console.log('Subscription closed:', reason);
|
||||
// Only reset state if the admin relay itself closed (not external relays timing out)
|
||||
// reason is an array of close reasons, one per relay in the subscription
|
||||
const adminUrl = relayConnectionUrl.value.trim();
|
||||
const closeNormalizedUrl = normalizeRelayUrlForPool(url);
|
||||
const closeConn = relayPool?._conn?.[url] || relayPool?._conn?.[closeNormalizedUrl];
|
||||
const closeRelay = relayPool?.relays?.get?.(closeNormalizedUrl);
|
||||
const reasons = Array.isArray(reason) ? reason : [reason];
|
||||
// Check if the admin relay's connection closed (not just external relays timing out)
|
||||
// If all reasons are 'connection timed out', these are external relays, not our relay
|
||||
const allTimeout = reasons.every(r => r === 'connection timed out');
|
||||
if (!allTimeout) {
|
||||
// Admin relay actually closed — reset state
|
||||
isSubscribed = false;
|
||||
isSubscribing = false;
|
||||
isRelayConnected = false;
|
||||
updateConfigStatus(false);
|
||||
log('WebSocket connection closed - subscription state reset', 'WARNING');
|
||||
} else {
|
||||
// External relays timed out — keep admin relay state intact
|
||||
log('External relay connections timed out (admin relay still connected)', 'INFO');
|
||||
}
|
||||
console.log('Subscription closed:', reason);
|
||||
console.log('🔎 ADMIN SUB DIAG: onclose details', {
|
||||
url,
|
||||
closeNormalizedUrl,
|
||||
reasons,
|
||||
allTimeout: reasons.length > 0 && reasons.every(r => String(r).includes('timeout')),
|
||||
hasConn: !!closeConn,
|
||||
hasWs: !!closeConn?.ws,
|
||||
wsReadyState: closeConn?.ws?.readyState,
|
||||
wsUrl: closeConn?.ws?.url,
|
||||
hasRelayObj: !!closeRelay,
|
||||
relayConnected: closeRelay?.connected,
|
||||
isLoggedIn,
|
||||
isRelayConnected,
|
||||
isSubscribed,
|
||||
isSubscribing
|
||||
});
|
||||
// This subscription is created only for the admin relay URL in subscribeMany([url], ...),
|
||||
// so any close/timeout here must reset admin relay state to force clean reconnect.
|
||||
isSubscribed = false;
|
||||
isSubscribing = false;
|
||||
isRelayConnected = false;
|
||||
updateConfigStatus(false);
|
||||
log('Admin relay subscription closed/timed out - connection state reset', 'WARNING');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1356,6 +1494,27 @@ async function subscribeToConfiguration() {
|
||||
isSubscribed = true;
|
||||
isSubscribing = false;
|
||||
|
||||
const normalizedUrlAfter = normalizeRelayUrlForPool(url);
|
||||
const relayMapKeysAfter = relayPool?.relays ? Array.from(relayPool.relays.keys()) : [];
|
||||
const postSubConn = relayPool._conn?.[url] || relayPool._conn?.[normalizedUrlAfter];
|
||||
const postSubRelay = relayPool?.relays?.get?.(normalizedUrlAfter);
|
||||
console.log('🔎 ADMIN SUB DIAG: post-subscribe connection snapshot', {
|
||||
url,
|
||||
normalizedUrl: normalizedUrlAfter,
|
||||
relayMapKeysAfter,
|
||||
hasConn: !!postSubConn,
|
||||
connType: postSubConn?.constructor?.name,
|
||||
hasWs: !!postSubConn?.ws,
|
||||
wsReadyState: postSubConn?.ws?.readyState,
|
||||
wsUrl: postSubConn?.ws?.url,
|
||||
wsProtocol: postSubConn?.ws?.protocol,
|
||||
hasRelayObj: !!postSubRelay,
|
||||
relayConnected: postSubRelay?.connected,
|
||||
isLoggedIn,
|
||||
isRelayConnected,
|
||||
isSubscribed,
|
||||
isSubscribing
|
||||
});
|
||||
console.log('✅ Subscription established successfully');
|
||||
return true;
|
||||
|
||||
@@ -1479,9 +1638,9 @@ function initializeEventRateChart() {
|
||||
debug: false // Disable debug logging
|
||||
});
|
||||
|
||||
console.log('ASCIIBarChart instance created:', eventRateChart);
|
||||
console.log('Chart container content after init:', chartContainer.textContent);
|
||||
console.log('Chart container innerHTML after init:', chartContainer.innerHTML);
|
||||
// console.log('ASCIIBarChart instance created:', eventRateChart);
|
||||
// console.log('Chart container content after init:', chartContainer.textContent);
|
||||
// console.log('Chart container innerHTML after init:', chartContainer.innerHTML);
|
||||
|
||||
// Force an initial render
|
||||
if (eventRateChart && typeof eventRateChart.render === 'function') {
|
||||
@@ -1965,7 +2124,7 @@ async function loadWotStatus() {
|
||||
|
||||
// Publish via SimplePool
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
await relayPool.publish([url], signedEvent);
|
||||
await relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
log('WoT status query sent successfully', 'INFO');
|
||||
|
||||
@@ -2011,7 +2170,7 @@ async function setWotLevel(level) {
|
||||
|
||||
// Publish via SimplePool
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
await relayPool.publish([url], signedEvent);
|
||||
await relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
log(`WoT level set to ${level} - sync will trigger automatically`, 'INFO');
|
||||
|
||||
@@ -2060,7 +2219,7 @@ async function syncWot() {
|
||||
|
||||
// Publish via SimplePool
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
await relayPool.publish([url], signedEvent);
|
||||
await relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
log('WoT sync command sent', 'INFO');
|
||||
|
||||
@@ -2243,7 +2402,7 @@ async function fetchConfiguration() {
|
||||
|
||||
// Publish via SimplePool with detailed error diagnostics
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -2268,6 +2427,13 @@ async function fetchConfiguration() {
|
||||
// Throw error if all relays failed
|
||||
if (successCount === 0) {
|
||||
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
||||
console.warn('❌ ADMIN COMMAND RESULT: FAILED (config_query)', {
|
||||
relayUrl: url,
|
||||
userPubkey,
|
||||
relayPubkey,
|
||||
reason: errorDetails,
|
||||
hint: 'Login can be successful while admin commands fail (wrong admin key, relay-side auth rejection, or command timeout).'
|
||||
});
|
||||
throw new Error(`All relays rejected the event. Details: ${errorDetails}`);
|
||||
}
|
||||
|
||||
@@ -2281,6 +2447,11 @@ async function fetchConfiguration() {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch configuration:', error);
|
||||
console.warn('❌ ADMIN COMMAND RESULT: FAILED (fetchConfiguration catch)', {
|
||||
userPubkey,
|
||||
relayPubkey,
|
||||
message: error?.message || String(error)
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2488,7 +2659,7 @@ async function sendConfigUpdateCommand(configObjects) {
|
||||
|
||||
// Publish via SimplePool with detailed error diagnostics
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -2684,7 +2855,7 @@ async function loadAuthRules() {
|
||||
|
||||
// Publish via SimplePool with detailed error diagnostics
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -2901,7 +3072,7 @@ async function deleteAuthRule(index) {
|
||||
|
||||
// Publish via SimplePool with detailed error diagnostics
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -3329,7 +3500,7 @@ async function addAuthRuleViaWebSocket(ruleData) {
|
||||
|
||||
// Publish via SimplePool with detailed error diagnostics
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -3435,7 +3606,7 @@ async function testGetAuthRules() {
|
||||
|
||||
// Publish via SimplePool with detailed error diagnostics
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -3509,7 +3680,7 @@ async function testClearAuthRules() {
|
||||
|
||||
// Publish via SimplePool with detailed error diagnostics
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -3592,7 +3763,7 @@ async function testAddBlacklist() {
|
||||
|
||||
// Publish via SimplePool with detailed error diagnostics
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -3675,7 +3846,7 @@ async function testAddWhitelist() {
|
||||
|
||||
// Publish via SimplePool
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -3749,7 +3920,7 @@ async function testConfigQuery() {
|
||||
|
||||
// Publish via SimplePool with detailed error diagnostics
|
||||
const url = relayUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -3819,7 +3990,7 @@ async function testPostEvent() {
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
logTestEvent('INFO', `Publishing to relay: ${url}`, 'INFO');
|
||||
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes instead of Promise.any
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -3997,7 +4168,7 @@ async function sendNIP17DM() {
|
||||
|
||||
// Publish via SimplePool
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedGiftWrap);
|
||||
const publishPromises = relayPool.publish([url], signedGiftWrap, { onauth: (challenge) => handleNip42Auth(url, challenge, 'nip17 dm publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -4235,7 +4406,7 @@ async function sendRestartCommand() {
|
||||
|
||||
// Publish via SimplePool
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -4286,6 +4457,12 @@ async function sendStatsQuery() {
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure admin subscription/connection is alive before publishing stats command
|
||||
const subscriptionReady = await subscribeToConfiguration();
|
||||
if (!subscriptionReady) {
|
||||
throw new Error('Admin relay subscription is not ready (connection may have timed out)');
|
||||
}
|
||||
|
||||
updateStatsStatus('loading', 'Querying database...');
|
||||
|
||||
// Create command array for stats query
|
||||
@@ -4316,7 +4493,28 @@ async function sendStatsQuery() {
|
||||
|
||||
// Publish via SimplePool
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const normalizedUrl = normalizeRelayUrlForPool(url);
|
||||
const relayMapKeysBeforePublish = relayPool?.relays ? Array.from(relayPool.relays.keys()) : [];
|
||||
const prePublishConn = relayPool._conn?.[url] || relayPool._conn?.[normalizedUrl];
|
||||
const prePublishRelay = relayPool?.relays?.get?.(normalizedUrl);
|
||||
console.log('🔎 ADMIN PUBLISH DIAG: pre-publish connection snapshot', {
|
||||
url,
|
||||
normalizedUrl,
|
||||
relayMapKeysBeforePublish,
|
||||
hasConn: !!prePublishConn,
|
||||
connType: prePublishConn?.constructor?.name,
|
||||
hasWs: !!prePublishConn?.ws,
|
||||
wsReadyState: prePublishConn?.ws?.readyState,
|
||||
wsUrl: prePublishConn?.ws?.url,
|
||||
wsProtocol: prePublishConn?.ws?.protocol,
|
||||
hasRelayObj: !!prePublishRelay,
|
||||
relayConnected: prePublishRelay?.connected,
|
||||
isLoggedIn,
|
||||
isRelayConnected,
|
||||
isSubscribed,
|
||||
isSubscribing
|
||||
});
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
@@ -4334,6 +4532,13 @@ async function sendStatsQuery() {
|
||||
|
||||
if (successCount === 0) {
|
||||
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
|
||||
console.warn('❌ ADMIN COMMAND RESULT: FAILED (stats_query)', {
|
||||
relayUrl: url,
|
||||
userPubkey,
|
||||
relayPubkey,
|
||||
reason: errorDetails,
|
||||
hint: 'Login can be successful while admin commands fail (wrong admin key, relay-side auth rejection, or command timeout).'
|
||||
});
|
||||
throw new Error(`All relays rejected stats query event. Details: ${errorDetails}`);
|
||||
}
|
||||
|
||||
@@ -4342,6 +4547,11 @@ async function sendStatsQuery() {
|
||||
|
||||
} catch (error) {
|
||||
log(`Failed to send stats query: ${error.message}`, 'ERROR');
|
||||
console.warn('❌ ADMIN COMMAND RESULT: FAILED (sendStatsQuery catch)', {
|
||||
userPubkey,
|
||||
relayPubkey,
|
||||
message: error?.message || String(error)
|
||||
});
|
||||
updateStatsStatus('error', error.message);
|
||||
}
|
||||
}
|
||||
@@ -4492,20 +4702,38 @@ function updateStatsFromCpuMonitoringEvent(monitoringData) {
|
||||
if (monitoringData.process_id !== undefined) {
|
||||
updateStatsCell('process-id', monitoringData.process_id.toString());
|
||||
}
|
||||
if (monitoringData.active_connections !== undefined) {
|
||||
updateStatsCell('websocket-connections', monitoringData.active_connections.toString());
|
||||
}
|
||||
|
||||
if (monitoringData.memory_usage_mb !== undefined) {
|
||||
updateStatsCell('memory-usage', monitoringData.memory_usage_mb.toFixed(1) + ' MB');
|
||||
}
|
||||
|
||||
// MEM% bar using memory_percent from server
|
||||
if (monitoringData.memory_percent !== undefined) {
|
||||
const memPct = Math.min(100, Math.max(0, monitoringData.memory_percent));
|
||||
updateStatsCell('memory-usage', makeAsciiBar(memPct) + ' ' + monitoringData.memory_usage_mb.toFixed(1) + ' MB');
|
||||
}
|
||||
|
||||
if (monitoringData.current_cpu_core !== undefined) {
|
||||
updateStatsCell('cpu-core', 'Core ' + monitoringData.current_cpu_core);
|
||||
}
|
||||
|
||||
// Calculate CPU usage percentage if we have the data
|
||||
// CPU% using delta between samples
|
||||
if (monitoringData.process_cpu_time !== undefined && monitoringData.system_cpu_time !== undefined) {
|
||||
// For now, just show the raw process CPU time (simplified)
|
||||
// In a real implementation, you'd calculate deltas over time
|
||||
updateStatsCell('cpu-usage', monitoringData.process_cpu_time + ' ticks');
|
||||
if (window._prevCpuSample) {
|
||||
const procDelta = monitoringData.process_cpu_time - window._prevCpuSample.proc;
|
||||
const sysDelta = monitoringData.system_cpu_time - window._prevCpuSample.sys;
|
||||
if (sysDelta > 0) {
|
||||
const cpuPct = Math.min(100, Math.max(0, (procDelta / sysDelta) * 100));
|
||||
updateStatsCell('cpu-usage', makeAsciiBar(cpuPct));
|
||||
}
|
||||
}
|
||||
window._prevCpuSample = {
|
||||
proc: monitoringData.process_cpu_time,
|
||||
sys: monitoringData.system_cpu_time
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -4895,6 +5123,13 @@ function formatTimestamp(timestamp) {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
// Generate ASCII progress bar: [###############.....] 75%
|
||||
function makeAsciiBar(pct) {
|
||||
const filled = Math.round(pct / 5); // 20 chars = 100%
|
||||
const empty = 20 - filled;
|
||||
return '[' + '#'.repeat(filled) + '.'.repeat(empty) + '] ' + pct.toFixed(1) + '%';
|
||||
}
|
||||
|
||||
// Update statistics cell with flash animation if value changed
|
||||
function updateStatsCell(cellId, newValue) {
|
||||
const cell = document.getElementById(cellId);
|
||||
@@ -5474,17 +5709,7 @@ async function sendAdminCommand(commandArray) {
|
||||
// Publish via SimplePool with NIP-42 auth support
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent, {
|
||||
async onauth(challenge) {
|
||||
console.log('🔐 NIP-42 AUTH challenge during publish:', challenge);
|
||||
try {
|
||||
const authEvent = window.NostrTools.nip42.makeAuthEvent(url, challenge);
|
||||
const signedAuthEvent = await window.nostr.signEvent(authEvent);
|
||||
console.log('✅ NIP-42 AUTH signed for publish');
|
||||
return signedAuthEvent;
|
||||
} catch (authError) {
|
||||
console.error('NIP-42 AUTH failed during publish:', authError.message);
|
||||
}
|
||||
}
|
||||
onauth: (challenge) => handleNip42Auth(url, challenge, 'admin publish')
|
||||
});
|
||||
|
||||
// Use Promise.allSettled to capture per-relay outcomes
|
||||
@@ -6042,7 +6267,7 @@ async function sendCreateRelayEventCommand(kind, eventData) {
|
||||
|
||||
// Publish via SimplePool
|
||||
const url = relayConnectionUrl.value.trim();
|
||||
const publishPromises = relayPool.publish([url], signedEvent);
|
||||
const publishPromises = relayPool.publish([url], signedEvent, { onauth: (challenge) => handleNip42Auth(url, challenge, 'publish') });
|
||||
|
||||
// Wait for publish results
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
|
||||
+95
-1
@@ -3297,8 +3297,18 @@ var NostrTools = (() => {
|
||||
this.enablePing = opts.enablePing;
|
||||
}
|
||||
async ensureRelay(url, params) {
|
||||
const rawUrl = url;
|
||||
const debugEnabled = typeof window !== "undefined" && !!window.__SIMPLE_POOL_DEBUG__;
|
||||
url = normalizeURL(url);
|
||||
let relay = this.relays.get(url);
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL ensureRelay START", {
|
||||
rawUrl,
|
||||
normalizedUrl: url,
|
||||
hadRelay: !!relay,
|
||||
relayMapKeysBefore: Array.from(this.relays.keys())
|
||||
});
|
||||
}
|
||||
if (!relay) {
|
||||
relay = new AbstractRelay(url, {
|
||||
verifyEvent: this.trustedRelayURLs.has(url) ? alwaysTrue : this.verifyEvent,
|
||||
@@ -3306,13 +3316,38 @@ var NostrTools = (() => {
|
||||
enablePing: this.enablePing
|
||||
});
|
||||
relay.onclose = () => {
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL ensureRelay onclose", {
|
||||
normalizedUrl: url,
|
||||
relayMapKeysBeforeDelete: Array.from(this.relays.keys())
|
||||
});
|
||||
}
|
||||
this.relays.delete(url);
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL ensureRelay onclose complete", {
|
||||
normalizedUrl: url,
|
||||
relayMapKeysAfterDelete: Array.from(this.relays.keys())
|
||||
});
|
||||
}
|
||||
};
|
||||
if (params?.connectionTimeout)
|
||||
relay.connectionTimeout = params.connectionTimeout;
|
||||
this.relays.set(url, relay);
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL ensureRelay created relay", {
|
||||
normalizedUrl: url,
|
||||
relayMapKeysAfterCreate: Array.from(this.relays.keys())
|
||||
});
|
||||
}
|
||||
}
|
||||
await relay.connect();
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL ensureRelay connected", {
|
||||
normalizedUrl: url,
|
||||
relayConnected: relay.connected,
|
||||
relayMapKeysAfterConnect: Array.from(this.relays.keys())
|
||||
});
|
||||
}
|
||||
return relay;
|
||||
}
|
||||
close(relays) {
|
||||
@@ -3334,16 +3369,26 @@ var NostrTools = (() => {
|
||||
}
|
||||
subscribeMany(relays, filters, params) {
|
||||
params.onauth = params.onauth || params.doauth;
|
||||
const debugEnabled = typeof window !== "undefined" && !!window.__SIMPLE_POOL_DEBUG__;
|
||||
const request = [];
|
||||
const uniqUrls = [];
|
||||
for (let i2 = 0; i2 < relays.length; i2++) {
|
||||
const url = normalizeURL(relays[i2]);
|
||||
if (uniqUrls.indexOf(url) === -1) {
|
||||
uniqUrls.push(url);
|
||||
for (let f2 = 0; f2 < filters.length; f2++) {
|
||||
request.push({ url, filter: filters[f2] });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL subscribeMany", {
|
||||
relaysInput: relays,
|
||||
uniqUrls,
|
||||
filtersCount: filters.length,
|
||||
requestsCount: request.length
|
||||
});
|
||||
}
|
||||
return this.subscribeMap(request, params);
|
||||
}
|
||||
subscribeMap(requests, params) {
|
||||
@@ -3391,14 +3436,31 @@ var NostrTools = (() => {
|
||||
_knownIds.add(id);
|
||||
return have;
|
||||
};
|
||||
const debugEnabled = typeof window !== "undefined" && !!window.__SIMPLE_POOL_DEBUG__;
|
||||
const allOpened = Promise.all(
|
||||
requests.map(async ({ url, filter }, i2) => {
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL subscribeMap request", {
|
||||
index: i2,
|
||||
url,
|
||||
filterKinds: filter?.kinds,
|
||||
hasAuthorFilter: !!filter?.authors,
|
||||
hasPTagFilter: !!filter?.["#p"]
|
||||
});
|
||||
}
|
||||
let relay;
|
||||
try {
|
||||
relay = await this.ensureRelay(url, {
|
||||
connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1e3) : void 0
|
||||
});
|
||||
} catch (err) {
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL subscribeMap ensureRelay FAILED", {
|
||||
index: i2,
|
||||
url,
|
||||
error: err?.message || String(err)
|
||||
});
|
||||
}
|
||||
handleClose(i2, err?.message || String(err));
|
||||
return;
|
||||
}
|
||||
@@ -3480,18 +3542,50 @@ var NostrTools = (() => {
|
||||
return events[0] || null;
|
||||
}
|
||||
publish(relays, event, options) {
|
||||
return relays.map(normalizeURL).map(async (url, i2, arr) => {
|
||||
const debugEnabled = typeof window !== "undefined" && !!window.__SIMPLE_POOL_DEBUG__;
|
||||
const normalizedRelays = relays.map(normalizeURL);
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL publish START", {
|
||||
relaysInput: relays,
|
||||
normalizedRelays,
|
||||
eventKind: event?.kind,
|
||||
eventId: event?.id
|
||||
});
|
||||
}
|
||||
return normalizedRelays.map(async (url, i2, arr) => {
|
||||
if (arr.indexOf(url) !== i2) {
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL publish duplicate URL", { url, index: i2 });
|
||||
}
|
||||
return Promise.reject("duplicate url");
|
||||
}
|
||||
let r = await this.ensureRelay(url);
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL publish ensured relay", {
|
||||
url,
|
||||
relayConnected: r?.connected,
|
||||
relayMapKeys: Array.from(this.relays.keys())
|
||||
});
|
||||
}
|
||||
return r.publish(event).catch(async (err) => {
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL publish ERROR", {
|
||||
url,
|
||||
error: err?.message || String(err)
|
||||
});
|
||||
}
|
||||
if (err instanceof Error && err.message.startsWith("auth-required: ") && options?.onauth) {
|
||||
await r.auth(options.onauth);
|
||||
return r.publish(event);
|
||||
}
|
||||
throw err;
|
||||
}).then((reason) => {
|
||||
if (debugEnabled) {
|
||||
console.log("🔎 SIMPLE_POOL publish SUCCESS", {
|
||||
url,
|
||||
reason
|
||||
});
|
||||
}
|
||||
if (this.trackRelays) {
|
||||
let set = this.seenOn.get(event.id);
|
||||
if (!set) {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
key,value,data_type,description,category,requires_restart,created_at,updated_at
|
||||
|
@@ -13,6 +13,7 @@ extern void log_query_execution(const char* query_type, const char* sub_id,
|
||||
#include <pthread.h>
|
||||
#include <libwebsockets.h>
|
||||
#include <cjson/cJSON.h>
|
||||
int get_active_connection_count(void);
|
||||
#include <sqlite3.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
@@ -1291,13 +1292,16 @@ cJSON* query_cpu_metrics(void) {
|
||||
pid_t pid = getpid();
|
||||
cJSON_AddNumberToObject(cpu_stats, "process_id", (double)pid);
|
||||
|
||||
// Get memory usage from /proc/self/status
|
||||
// Get active WebSocket connection count
|
||||
cJSON_AddNumberToObject(cpu_stats, "active_connections", (double)get_active_connection_count());
|
||||
|
||||
// Get memory usage from /proc/self/status and calculate MEM%
|
||||
unsigned long rss_kb = 0;
|
||||
FILE* mem_stat = fopen("/proc/self/status", "r");
|
||||
if (mem_stat) {
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), mem_stat)) {
|
||||
if (strncmp(line, "VmRSS:", 6) == 0) {
|
||||
unsigned long rss_kb;
|
||||
if (sscanf(line, "VmRSS: %lu kB", &rss_kb) == 1) {
|
||||
double rss_mb = rss_kb / 1024.0;
|
||||
cJSON_AddNumberToObject(cpu_stats, "memory_usage_mb", rss_mb);
|
||||
@@ -1308,6 +1312,25 @@ cJSON* query_cpu_metrics(void) {
|
||||
fclose(mem_stat);
|
||||
}
|
||||
|
||||
// Get total system memory from /proc/meminfo for MEM%
|
||||
if (rss_kb > 0) {
|
||||
FILE* meminfo = fopen("/proc/meminfo", "r");
|
||||
if (meminfo) {
|
||||
char line[256];
|
||||
while (fgets(line, sizeof(line), meminfo)) {
|
||||
if (strncmp(line, "MemTotal:", 9) == 0) {
|
||||
unsigned long total_kb;
|
||||
if (sscanf(line, "MemTotal: %lu kB", &total_kb) == 1 && total_kb > 0) {
|
||||
double mem_pct = (rss_kb * 100.0) / total_kb;
|
||||
cJSON_AddNumberToObject(cpu_stats, "memory_percent", mem_pct);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(meminfo);
|
||||
}
|
||||
}
|
||||
|
||||
return cpu_stats;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,8 +101,8 @@ static const struct {
|
||||
|
||||
// Idle Connection Ban Settings
|
||||
// Ban IPs that connect but never send REQ or EVENT (idle or early disconnect)
|
||||
{"idle_connection_timeout_sec", "30"}, // Seconds before idle connection is closed (0 = disabled)
|
||||
{"idle_ban_enabled", "true"}, // Whether to ban IPs with idle failures
|
||||
{"idle_connection_timeout_sec", "0"}, // Seconds before idle connection is closed (0 = disabled)
|
||||
{"idle_ban_enabled", "false"}, // Whether to ban IPs with idle failures
|
||||
{"idle_ban_threshold", "1"}, // Idle failures before ban (1 = ban on first offense)
|
||||
{"idle_ban_window_sec", "30"}, // Window to count idle failures in
|
||||
{"idle_ban_duration_sec", "300"}, // Initial ban duration (doubles each time, max 24h)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1260,6 +1260,13 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
continue;
|
||||
}
|
||||
|
||||
// NIP-01: limit:0 means return zero stored events — skip SQL entirely, just send EOSE
|
||||
cJSON* limit_check = cJSON_GetObjectItemCaseSensitive(filter, "limit");
|
||||
if (limit_check && cJSON_IsNumber(limit_check) && (int)cJSON_GetNumberValue(limit_check) == 0) {
|
||||
DEBUG_LOG("Filter has limit:0 — skipping query, sending EOSE immediately (NIP-01 compliance)");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset bind params for this filter
|
||||
free_bind_params(bind_params, bind_param_count);
|
||||
bind_params = NULL;
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@
|
||||
// Using CRELAY_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||
#define CRELAY_VERSION_MAJOR 1
|
||||
#define CRELAY_VERSION_MINOR 2
|
||||
#define CRELAY_VERSION_PATCH 44
|
||||
#define CRELAY_VERSION "v1.2.44"
|
||||
#define CRELAY_VERSION_PATCH 51
|
||||
#define CRELAY_VERSION "v1.2.51"
|
||||
|
||||
// Relay metadata (authoritative source for NIP-11 information)
|
||||
#define RELAY_NAME "C-Relay"
|
||||
|
||||
+11
-1
@@ -12,6 +12,7 @@
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <stdio.h>
|
||||
#include "websockets.h"
|
||||
#include "ip_ban.h"
|
||||
|
||||
@@ -94,8 +95,17 @@ void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* ps
|
||||
|
||||
// Verify authentication using existing request_validator function
|
||||
// Note: nostr_nip42_verify_auth_event doesn't extract pubkey, we need to do that separately
|
||||
char relay_url[RELAY_URL_MAX_LENGTH];
|
||||
const char* configured_relay_url = get_config_value("relay_url");
|
||||
if (configured_relay_url && configured_relay_url[0] != '\0') {
|
||||
snprintf(relay_url, sizeof(relay_url), "%s", configured_relay_url);
|
||||
} else {
|
||||
int relay_port = (g_relay_port > 0) ? g_relay_port : get_config_int("relay_port", DEFAULT_PORT);
|
||||
snprintf(relay_url, sizeof(relay_url), "ws://127.0.0.1:%d", relay_port);
|
||||
}
|
||||
|
||||
int result = nostr_nip42_verify_auth_event(auth_event, challenge_copy,
|
||||
"ws://localhost:8888", 600); // 10 minutes tolerance
|
||||
relay_url, 600); // 10 minutes tolerance
|
||||
|
||||
char authenticated_pubkey[65] = {0};
|
||||
if (result == 0) {
|
||||
|
||||
@@ -116,6 +116,9 @@ extern struct lws_context *ws_context;
|
||||
// Global subscription manager
|
||||
struct subscription_manager g_subscription_manager;
|
||||
|
||||
// Actual relay port bound by libwebsockets at runtime (after fallback/retry logic)
|
||||
int g_relay_port = DEFAULT_PORT;
|
||||
|
||||
// Global connection list for idle connection tracking
|
||||
// Tracks ALL WebSocket connections (not just subscribed ones)
|
||||
// so the periodic timer can find and close idle connections
|
||||
@@ -130,6 +133,8 @@ static tracked_connection_t g_connections[MAX_TRACKED_CONNECTIONS];
|
||||
static int g_connection_count = 0;
|
||||
static pthread_mutex_t g_connections_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
int get_active_connection_count(void) { return g_connection_count; }
|
||||
|
||||
static void connection_list_add(struct lws* wsi, struct per_session_data* pss) {
|
||||
pthread_mutex_lock(&g_connections_lock);
|
||||
for (int i = 0; i < MAX_TRACKED_CONNECTIONS; i++) {
|
||||
@@ -2607,6 +2612,9 @@ int start_websocket_relay(int port_override, int strict_port) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Persist the actual bound port for components that need the runtime relay URL
|
||||
g_relay_port = actual_port;
|
||||
|
||||
char startup_msg[256];
|
||||
if (actual_port != configured_port) {
|
||||
snprintf(startup_msg, sizeof(startup_msg),
|
||||
|
||||
@@ -96,6 +96,12 @@ struct per_session_data {
|
||||
int is_websocket; // 1 if this is a WebSocket connection, 0 for HTTP
|
||||
};
|
||||
|
||||
// Get current active WebSocket connection count
|
||||
int get_active_connection_count(void);
|
||||
|
||||
// Actual relay port bound by libwebsockets at runtime (after fallback/retry logic)
|
||||
extern int g_relay_port;
|
||||
|
||||
// NIP-11 HTTP session data structure for managing buffer lifetime
|
||||
struct nip11_session_data {
|
||||
int type; // 0 for NIP-11
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import WebSocket from 'ws';
|
||||
import {
|
||||
finalizeEvent,
|
||||
generateSecretKey,
|
||||
getPublicKey,
|
||||
nip19,
|
||||
nip42,
|
||||
} from 'nostr-tools';
|
||||
|
||||
const RELAY_URL = process.env.RELAY_URL || 'ws://127.0.0.1:7777';
|
||||
const EVENT_COUNT = Number(process.env.EVENT_COUNT || 5);
|
||||
const DELAY_MS = Number(process.env.DELAY_MS || 2000);
|
||||
const RECIPIENT_PUBKEY_ENV = process.env.RECIPIENT_PUBKEY || '';
|
||||
const SECRET_INPUT = process.env.NOSTR_SECRET_KEY || '';
|
||||
const OVERALL_TIMEOUT_MS = Number(process.env.OVERALL_TIMEOUT_MS || 120000);
|
||||
|
||||
function now() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function log(msg, extra = null) {
|
||||
if (extra !== null) {
|
||||
console.log(`[${now()}] ${msg}`, extra);
|
||||
} else {
|
||||
console.log(`[${now()}] ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeHexSecret(secretInput) {
|
||||
if (!secretInput) return null;
|
||||
|
||||
const raw = secretInput.trim();
|
||||
if (/^[0-9a-fA-F]{64}$/.test(raw)) {
|
||||
return raw.toLowerCase();
|
||||
}
|
||||
|
||||
if (raw.startsWith('nsec1')) {
|
||||
const decoded = nip19.decode(raw);
|
||||
if (decoded.type !== 'nsec') {
|
||||
throw new Error('NOSTR_SECRET_KEY is nsec-like but did not decode as nsec');
|
||||
}
|
||||
if (typeof decoded.data === 'string') {
|
||||
return decoded.data.toLowerCase();
|
||||
}
|
||||
if (decoded.data instanceof Uint8Array) {
|
||||
return Buffer.from(decoded.data).toString('hex').toLowerCase();
|
||||
}
|
||||
throw new Error('Unsupported nsec decoded payload type');
|
||||
}
|
||||
|
||||
throw new Error('NOSTR_SECRET_KEY must be 64-char hex or nsec1...');
|
||||
}
|
||||
|
||||
const hexSecret = normalizeHexSecret(SECRET_INPUT) || Buffer.from(generateSecretKey()).toString('hex');
|
||||
const secretKey = Buffer.from(hexSecret, 'hex');
|
||||
const senderPubkey = getPublicKey(secretKey);
|
||||
const recipientPubkey = RECIPIENT_PUBKEY_ENV || senderPubkey;
|
||||
|
||||
let ws;
|
||||
let currentChallenge = null;
|
||||
let authenticated = false;
|
||||
let authAttempts = 0;
|
||||
let sentAccepted = 0;
|
||||
let nextSeq = 1;
|
||||
let pendingEvent = null;
|
||||
let waitingForAuth = false;
|
||||
let finished = false;
|
||||
let overallTimer = null;
|
||||
|
||||
function makeKind4Event(seq) {
|
||||
const eventTemplate = {
|
||||
kind: 4,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['p', recipientPubkey]],
|
||||
content: `kind4 disconnect probe seq=${seq} ts=${Date.now()}`,
|
||||
pubkey: senderPubkey,
|
||||
};
|
||||
return finalizeEvent(eventTemplate, secretKey);
|
||||
}
|
||||
|
||||
function sendJson(arr) {
|
||||
const payload = JSON.stringify(arr);
|
||||
ws.send(payload);
|
||||
}
|
||||
|
||||
function sendAuth(challenge) {
|
||||
const authTemplate = nip42.makeAuthEvent(RELAY_URL, challenge);
|
||||
const signedAuth = finalizeEvent(authTemplate, secretKey);
|
||||
authAttempts += 1;
|
||||
log(`Sending AUTH attempt #${authAttempts} with challenge ${challenge}`);
|
||||
sendJson(['AUTH', signedAuth]);
|
||||
}
|
||||
|
||||
function scheduleNext() {
|
||||
if (nextSeq > EVENT_COUNT) {
|
||||
log(`All ${EVENT_COUNT} kind-4 events were accepted without disconnect`);
|
||||
cleanupAndExit(0);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (!finished) {
|
||||
publishNext();
|
||||
}
|
||||
}, DELAY_MS);
|
||||
}
|
||||
|
||||
function publishNext() {
|
||||
if (waitingForAuth) {
|
||||
log('Still waiting for auth completion, postponing publish');
|
||||
return;
|
||||
}
|
||||
|
||||
const ev = makeKind4Event(nextSeq);
|
||||
pendingEvent = ev;
|
||||
log(`Publishing kind-4 seq=${nextSeq} id=${ev.id.slice(0, 12)}...`);
|
||||
sendJson(['EVENT', ev]);
|
||||
}
|
||||
|
||||
function retryPendingAfterAuth() {
|
||||
if (!pendingEvent) return;
|
||||
waitingForAuth = false;
|
||||
log(`Retrying pending kind-4 id=${pendingEvent.id.slice(0, 12)}... after auth`);
|
||||
sendJson(['EVENT', pendingEvent]);
|
||||
}
|
||||
|
||||
function handleAuthRequiredSignal(source, msg) {
|
||||
waitingForAuth = true;
|
||||
log(`Relay signaled auth-required via ${source}: ${msg}`);
|
||||
if (currentChallenge) {
|
||||
sendAuth(currentChallenge);
|
||||
} else {
|
||||
log('No AUTH challenge received yet; waiting for relay AUTH challenge message');
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupAndExit(code) {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
if (overallTimer) clearTimeout(overallTimer);
|
||||
|
||||
const summary = {
|
||||
relay: RELAY_URL,
|
||||
sentAccepted,
|
||||
expected: EVENT_COUNT,
|
||||
authAttempts,
|
||||
authenticated,
|
||||
senderPubkey,
|
||||
recipientPubkey,
|
||||
};
|
||||
|
||||
if (code === 0) {
|
||||
log('PASS: Connection remained stable through repeated kind-4 publish cycle', summary);
|
||||
} else {
|
||||
log('FAIL: Relay disconnected or test did not complete successfully', summary);
|
||||
}
|
||||
|
||||
try {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function main() {
|
||||
log('Starting kind-4 disconnect probe', {
|
||||
relay: RELAY_URL,
|
||||
eventCount: EVENT_COUNT,
|
||||
delayMs: DELAY_MS,
|
||||
senderPubkey,
|
||||
recipientPubkey,
|
||||
});
|
||||
|
||||
ws = new WebSocket(RELAY_URL);
|
||||
|
||||
overallTimer = setTimeout(() => {
|
||||
log(`Overall timeout reached (${OVERALL_TIMEOUT_MS}ms)`);
|
||||
cleanupAndExit(1);
|
||||
}, OVERALL_TIMEOUT_MS);
|
||||
|
||||
ws.on('open', () => {
|
||||
log('WebSocket connected');
|
||||
publishNext();
|
||||
});
|
||||
|
||||
ws.on('message', (raw) => {
|
||||
const text = raw.toString();
|
||||
let msg;
|
||||
try {
|
||||
msg = JSON.parse(text);
|
||||
} catch {
|
||||
log(`Non-JSON relay message: ${text}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const t = msg?.[0];
|
||||
|
||||
if (t === 'AUTH') {
|
||||
currentChallenge = msg?.[1] || null;
|
||||
log(`Received AUTH challenge: ${currentChallenge}`);
|
||||
if (currentChallenge) sendAuth(currentChallenge);
|
||||
return;
|
||||
}
|
||||
|
||||
if (t === 'NOTICE') {
|
||||
const notice = String(msg?.[1] || '');
|
||||
log(`NOTICE: ${notice}`);
|
||||
|
||||
if (notice.toLowerCase().includes('auth-required')) {
|
||||
handleAuthRequiredSignal('NOTICE', notice);
|
||||
return;
|
||||
}
|
||||
|
||||
if (notice.toLowerCase().includes('authentication successful')) {
|
||||
authenticated = true;
|
||||
log('Relay confirmed NIP-42 authentication success');
|
||||
retryPendingAfterAuth();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (t === 'OK') {
|
||||
const eventId = msg?.[1];
|
||||
const ok = !!msg?.[2];
|
||||
const reason = String(msg?.[3] || '');
|
||||
log(`OK for ${String(eventId).slice(0, 12)}... accepted=${ok} reason=${reason}`);
|
||||
|
||||
if (!ok && reason.toLowerCase().includes('auth-required')) {
|
||||
handleAuthRequiredSignal('OK', reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingEvent && eventId === pendingEvent.id) {
|
||||
if (ok) {
|
||||
sentAccepted += 1;
|
||||
nextSeq += 1;
|
||||
pendingEvent = null;
|
||||
waitingForAuth = false;
|
||||
scheduleNext();
|
||||
} else {
|
||||
log(`Pending event rejected: ${reason}`);
|
||||
cleanupAndExit(1);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log('Relay message', msg);
|
||||
});
|
||||
|
||||
ws.on('close', (code, reasonBuf) => {
|
||||
const reason = reasonBuf?.toString?.() || '';
|
||||
log(`WebSocket closed code=${code} reason=${reason}`);
|
||||
if (!finished) {
|
||||
if (sentAccepted >= EVENT_COUNT) {
|
||||
cleanupAndExit(0);
|
||||
} else {
|
||||
cleanupAndExit(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', (err) => {
|
||||
log(`WebSocket error: ${err.message}`);
|
||||
cleanupAndExit(1);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user