v1.2.50 - Fix NIP-42 relay URL verification and add onauth to all publish flows

This commit is contained in:
Your Name
2026-03-01 12:20:44 -04:00
parent 927659ece1
commit 3265e3d114
10 changed files with 434 additions and 120 deletions
+310 -110
View File
@@ -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);
}
}
@@ -5499,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
@@ -6067,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
View File
@@ -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) {
+1
View File
@@ -0,0 +1 @@
key,value,data_type,description,category,requires_restart,created_at,updated_at
1 key value data_type description category requires_restart created_at updated_at
+1 -1
View File
@@ -1 +1 @@
2391505
2946182
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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 49
#define CRELAY_VERSION "v1.2.49"
#define CRELAY_VERSION_PATCH 50
#define CRELAY_VERSION "v1.2.50"
// Relay metadata (authoritative source for NIP-11 information)
#define RELAY_NAME "C-Relay"
+11 -1
View File
@@ -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) {
+6
View File
@@ -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
@@ -2609,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),
+3
View File
@@ -99,6 +99,9 @@ struct per_session_data {
// 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