From 3210b9e75270117ed24dccb0796ea31dac203c50 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 16 Sep 2025 15:52:27 -0400 Subject: [PATCH] v0.3.7 - working on cinfig api --- api/index.html | 286 +++++++++-------- api/nostr-lite.js | 360 +++++++++++++++++---- relay.pid | 2 +- src/config.c | 797 +++++++++++++++++++++++++++++++++++++++++++++- src/config.h | 39 +++ src/main.c | 51 ++- src/sql_schema.h | 64 +++- test_relay.js | 191 ----------- 8 files changed, 1373 insertions(+), 417 deletions(-) delete mode 100644 test_relay.js diff --git a/api/index.html b/api/index.html index fc53630..1463260 100644 --- a/api/index.html +++ b/api/index.html @@ -1,5 +1,6 @@ + @@ -10,7 +11,7 @@ padding: 0; box-sizing: border-box; } - + body { font-family: 'Courier New', monospace; background-color: white; @@ -20,7 +21,7 @@ max-width: 1200px; margin: 0 auto; } - + h1 { border-bottom: 2px solid black; padding-bottom: 10px; @@ -28,7 +29,7 @@ font-weight: normal; font-size: 24px; } - + h2 { margin: 30px 0 15px 0; font-weight: normal; @@ -36,25 +37,28 @@ padding-left: 10px; font-size: 16px; } - + .section { border: 1px solid black; padding: 20px; margin-bottom: 20px; } - + .input-group { margin-bottom: 15px; } - + label { display: block; margin-bottom: 5px; font-weight: bold; font-size: 14px; } - - input, textarea, select, button { + + input, + textarea, + select, + button { width: 100%; padding: 8px; border: 1px solid black; @@ -63,7 +67,7 @@ background-color: white; color: black; } - + button { background-color: black; color: white; @@ -71,65 +75,65 @@ margin: 5px 0; font-weight: bold; } - + button:hover { background-color: #333; } - + button:disabled { background-color: #ccc; color: #666; cursor: not-allowed; } - + .status { padding: 10px; margin: 10px 0; border: 1px solid black; font-weight: bold; } - + .status.connected { background-color: black; color: white; } - + .status.disconnected { background-color: white; color: black; } - + .status.authenticated { background-color: black; color: white; } - + .status.error { background-color: white; color: black; border: 2px solid black; } - + .config-table { border: 1px solid black; width: 100%; border-collapse: collapse; margin: 10px 0; } - + .config-table th, .config-table td { border: 1px solid black; padding: 8px; text-align: left; } - + .config-table th { background-color: black; color: white; font-weight: bold; } - + .json-display { background-color: white; border: 1px solid black; @@ -141,7 +145,7 @@ overflow-y: auto; margin: 10px 0; } - + .log-panel { height: 200px; overflow-y: auto; @@ -150,71 +154,72 @@ font-size: 12px; background-color: white; } - + .log-entry { margin-bottom: 5px; border-bottom: 1px solid #ccc; padding-bottom: 5px; } - + .log-timestamp { font-weight: bold; } - + .inline-buttons { display: flex; gap: 10px; } - + .inline-buttons button { flex: 1; } - + .user-info { padding: 10px; border: 1px solid black; margin: 10px 0; background-color: white; } - + .user-pubkey { font-family: 'Courier New', monospace; font-size: 12px; word-break: break-all; margin: 5px 0; } - + .hidden { display: none; } - + #login-section { text-align: center; padding: 20px; } - + @media (max-width: 768px) { body { padding: 10px; } - + .inline-buttons { flex-direction: column; } - + h1 { font-size: 20px; } - + h2 { font-size: 14px; } } +

C-RELAY ADMIN API

- +

DEBUG - TEST FETCH WITHOUT LOGIN

@@ -225,7 +230,7 @@
READY TO FETCH
NO CONFIGURATION LOADED
- + - +
@@ -342,10 +349,10 @@ - + + \ No newline at end of file diff --git a/api/nostr-lite.js b/api/nostr-lite.js index 2717e91..87a81ee 100644 --- a/api/nostr-lite.js +++ b/api/nostr-lite.js @@ -8,7 +8,7 @@ * Two-file architecture: * 1. Load nostr.bundle.js (official nostr-tools bundle) * 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes) - * Generated on: 2025-09-15T18:50:50.789Z + * Generated on: 2025-09-16T15:52:30.145Z */ // Verify dependencies are loaded @@ -1128,23 +1128,62 @@ class Modal { } _handleExtension() { - // Detect all available real extensions - const availableExtensions = this._detectAllExtensions(); + // SIMPLIFIED ARCHITECTURE: Check for single extension at window.nostr or preserved extension + let extension = null; - console.log(`Modal: Found ${availableExtensions.length} extensions:`, availableExtensions.map(e => e.displayName)); - - if (availableExtensions.length === 0) { - console.log('Modal: No real extensions found'); - this._showExtensionRequired(); - } else if (availableExtensions.length === 1) { - // Single extension - use it directly without showing choice UI - console.log('Modal: Single extension detected, using it directly:', availableExtensions[0].displayName); - this._tryExtensionLogin(availableExtensions[0].extension); - } else { - // Multiple extensions - show choice UI - console.log('Modal: Multiple extensions detected, showing choice UI for', availableExtensions.length, 'extensions'); - this._showExtensionChoice(availableExtensions); + // Check if NostrLite instance has a preserved extension (real extension detected at init) + if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) { + extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension; + console.log('Modal: Using preserved extension:', extension.constructor?.name); } + // Otherwise check current window.nostr + else if (window.nostr && this._isRealExtension(window.nostr)) { + extension = window.nostr; + console.log('Modal: Using current window.nostr extension:', extension.constructor?.name); + } + + if (!extension) { + console.log('Modal: No extension detected yet, waiting for deferred detection...'); + + // DEFERRED EXTENSION CHECK: Extensions like nos2x might load after our library + let attempts = 0; + const maxAttempts = 10; // Try for 2 seconds + const checkForExtension = () => { + attempts++; + + // Check again for preserved extension (might be set by deferred detection) + if (window.NOSTR_LOGIN_LITE?._instance?.preservedExtension) { + extension = window.NOSTR_LOGIN_LITE._instance.preservedExtension; + console.log('Modal: Found preserved extension after waiting:', extension.constructor?.name); + this._tryExtensionLogin(extension); + return; + } + + // Check current window.nostr again + if (window.nostr && this._isRealExtension(window.nostr)) { + extension = window.nostr; + console.log('Modal: Found extension at window.nostr after waiting:', extension.constructor?.name); + this._tryExtensionLogin(extension); + return; + } + + // Keep trying or give up + if (attempts < maxAttempts) { + setTimeout(checkForExtension, 200); + } else { + console.log('Modal: No browser extension found after waiting 2 seconds'); + this._showExtensionRequired(); + } + }; + + // Start checking after a brief delay + setTimeout(checkForExtension, 200); + return; + } + + // Use the single detected extension directly - no choice UI + console.log('Modal: Single extension mode - using extension directly'); + this._tryExtensionLogin(extension); } _detectAllExtensions() { @@ -1190,17 +1229,38 @@ class Modal { // Also check window.nostr but be extra careful to avoid our library console.log('Modal: Checking window.nostr:', !!window.nostr, window.nostr?.constructor?.name); - if (window.nostr && this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) { - extensions.push({ - name: 'window.nostr', - displayName: 'Extension (window.nostr)', - icon: '🔑', - extension: window.nostr - }); - seenExtensions.add(window.nostr); - console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`); - } else if (window.nostr) { - console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - likely our library`); + + if (window.nostr) { + // Check if window.nostr is our WindowNostr facade with a preserved extension + if (window.nostr.constructor?.name === 'WindowNostr' && window.nostr.existingNostr) { + console.log('Modal: Found WindowNostr facade, checking existingNostr for preserved extension'); + const preservedExtension = window.nostr.existingNostr; + console.log('Modal: Preserved extension:', !!preservedExtension, preservedExtension?.constructor?.name); + + if (preservedExtension && this._isRealExtension(preservedExtension) && !seenExtensions.has(preservedExtension)) { + extensions.push({ + name: 'window.nostr.existingNostr', + displayName: 'Extension (preserved by WindowNostr)', + icon: '🔑', + extension: preservedExtension + }); + seenExtensions.add(preservedExtension); + console.log(`Modal: ✓ Detected preserved extension: ${preservedExtension.constructor?.name}`); + } + } + // Check if window.nostr is directly a real extension (not our facade) + else if (this._isRealExtension(window.nostr) && !seenExtensions.has(window.nostr)) { + extensions.push({ + name: 'window.nostr', + displayName: 'Extension (window.nostr)', + icon: '🔑', + extension: window.nostr + }); + seenExtensions.add(window.nostr); + console.log(`Modal: ✓ Detected extension at window.nostr: ${window.nostr.constructor?.name}`); + } else { + console.log(`Modal: ✗ Filtered out window.nostr (${window.nostr.constructor?.name}) - not a real extension`); + } } return extensions; @@ -1790,6 +1850,63 @@ class Modal { } _setAuthMethod(method, options = {}) { + // SINGLE-EXTENSION ARCHITECTURE: Handle method switching + console.log('Modal: _setAuthMethod called with:', method, options); + + // CRITICAL: Never install facade for extension methods - leave window.nostr as the extension + if (method === 'extension') { + console.log('Modal: Extension method - NOT installing facade, leaving window.nostr as extension'); + + // Emit auth method selection directly for extension + const event = new CustomEvent('nlMethodSelected', { + detail: { method, ...options } + }); + window.dispatchEvent(event); + this.close(); + return; + } + + // For non-extension methods, we need to ensure WindowNostr facade is available + console.log('Modal: Non-extension method detected:', method); + + // Check if we have a preserved extension but no WindowNostr facade installed + const hasPreservedExtension = !!window.NOSTR_LOGIN_LITE?._instance?.preservedExtension; + const hasWindowNostrFacade = window.nostr?.constructor?.name === 'WindowNostr'; + + console.log('Modal: Method switching check:'); + console.log(' method:', method); + console.log(' hasPreservedExtension:', hasPreservedExtension); + console.log(' hasWindowNostrFacade:', hasWindowNostrFacade); + console.log(' current window.nostr constructor:', window.nostr?.constructor?.name); + + // If we have a preserved extension but no facade, install facade for method switching + if (hasPreservedExtension && !hasWindowNostrFacade) { + console.log('Modal: Installing WindowNostr facade for method switching (non-extension authentication)'); + + // Get the NostrLite instance and install facade with preserved extension + const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance; + if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') { + const preservedExtension = nostrLiteInstance.preservedExtension; + console.log('Modal: Installing facade with preserved extension:', preservedExtension?.constructor?.name); + + nostrLiteInstance._installFacade(preservedExtension); + console.log('Modal: WindowNostr facade installed for method switching'); + } else { + console.error('Modal: Cannot access NostrLite instance or _installFacade method'); + } + } + + // If no extension at all, ensure facade is installed for local/NIP-46/readonly methods + else if (!hasPreservedExtension && !hasWindowNostrFacade) { + console.log('Modal: Installing WindowNostr facade for non-extension methods (no extension detected)'); + + const nostrLiteInstance = window.NOSTR_LOGIN_LITE?._instance; + if (nostrLiteInstance && typeof nostrLiteInstance._installFacade === 'function') { + nostrLiteInstance._installFacade(); + console.log('Modal: WindowNostr facade installed for non-extension methods'); + } + } + // Emit auth method selection const event = new CustomEvent('nlMethodSelected', { detail: { method, ...options } @@ -1823,8 +1940,13 @@ class Modal { title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;'; const message = document.createElement('p'); - message.textContent = 'Please install a Nostr browser extension like Alby or getflattr and refresh the page.'; - message.style.cssText = 'margin-bottom: 20px; color: #6b7280;'; + message.innerHTML = ` + Please install a Nostr browser extension and refresh the page.

+ Important: If you have multiple extensions installed, please disable all but one to avoid conflicts. +

+ Popular extensions: Alby, nos2x, Flamingo + `; + message.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px; line-height: 1.4;'; const backButton = document.createElement('button'); backButton.textContent = 'Back'; @@ -1867,27 +1989,11 @@ class Modal { box-sizing: border-box; `; - const urlLabel = document.createElement('label'); - urlLabel.textContent = 'Remote URL (optional):'; - urlLabel.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500;'; - - const urlInput = document.createElement('input'); - urlInput.type = 'url'; - urlInput.placeholder = 'ws://localhost:8080 (default)'; - urlInput.style.cssText = ` - width: 100%; - padding: 12px; - border: 1px solid #d1d5db; - border-radius: 6px; - margin-bottom: 16px; - box-sizing: border-box; - `; - - // Users will enter the bunker URL manually from their bunker setup + // Users will enter the complete bunker connection string with relay info const connectButton = document.createElement('button'); connectButton.textContent = 'Connect to Bunker'; - connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value, urlInput.value); + connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value); connectButton.style.cssText = this._getButtonStyle(); const backButton = document.createElement('button'); @@ -1897,8 +2003,6 @@ class Modal { formGroup.appendChild(label); formGroup.appendChild(pubkeyInput); - formGroup.appendChild(urlLabel); - formGroup.appendChild(urlInput); this.modalBody.appendChild(title); this.modalBody.appendChild(description); @@ -1907,17 +2011,17 @@ class Modal { this.modalBody.appendChild(backButton); } - _handleNip46Connect(bunkerPubkey, bunkerUrl) { + _handleNip46Connect(bunkerPubkey) { if (!bunkerPubkey || !bunkerPubkey.length) { this._showError('Bunker pubkey is required'); return; } - this._showNip46Connecting(bunkerPubkey, bunkerUrl); - this._performNip46Connect(bunkerPubkey, bunkerUrl); + this._showNip46Connecting(bunkerPubkey); + this._performNip46Connect(bunkerPubkey); } - _showNip46Connecting(bunkerPubkey, bunkerUrl) { + _showNip46Connecting(bunkerPubkey) { this.modalBody.innerHTML = ''; const title = document.createElement('h3'); @@ -1935,9 +2039,8 @@ class Modal { bunkerInfo.style.cssText = 'background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 14px;'; bunkerInfo.innerHTML = ` Connecting to bunker:
- Pubkey: ${displayPubkey}
- Relay: ${bunkerUrl || 'ws://localhost:8080'}
- If this relay is offline, the bunker server may be unavailable. + Connection: ${displayPubkey}
+ Connection string contains all necessary relay information. `; const connectingDiv = document.createElement('div'); @@ -1954,9 +2057,9 @@ class Modal { this.modalBody.appendChild(connectingDiv); } - async _performNip46Connect(bunkerPubkey, bunkerUrl) { + async _performNip46Connect(bunkerPubkey) { try { - console.log('Starting NIP-46 connection to bunker:', bunkerPubkey, bunkerUrl); + console.log('Starting NIP-46 connection to bunker:', bunkerPubkey); // Check if nostr-tools NIP-46 is available if (!window.NostrTools?.nip46) { @@ -2648,16 +2751,149 @@ class NostrLite { _setupWindowNostrFacade() { if (typeof window !== 'undefined') { + console.log('NOSTR_LOGIN_LITE: === TRUE SINGLE-EXTENSION ARCHITECTURE ==='); + console.log('NOSTR_LOGIN_LITE: Initial window.nostr:', window.nostr); + console.log('NOSTR_LOGIN_LITE: Initial window.nostr constructor:', window.nostr?.constructor?.name); + // Store existing window.nostr if it exists (from extensions) const existingNostr = window.nostr; - // Always install our facade - window.nostr = new WindowNostr(this, existingNostr); - console.log('NOSTR_LOGIN_LITE: window.nostr facade installed', - existingNostr ? '(with extension passthrough)' : '(no existing extension)'); + // TRUE SINGLE-EXTENSION ARCHITECTURE: Don't install facade when extensions detected + if (this._isRealExtension(existingNostr)) { + console.log('NOSTR_LOGIN_LITE: ✓ REAL EXTENSION DETECTED IMMEDIATELY - PRESERVING WITHOUT FACADE'); + console.log('NOSTR_LOGIN_LITE: Extension constructor:', existingNostr.constructor?.name); + console.log('NOSTR_LOGIN_LITE: Extension keys:', Object.keys(existingNostr)); + console.log('NOSTR_LOGIN_LITE: Leaving window.nostr untouched for extension compatibility'); + this.preservedExtension = existingNostr; + this.facadeInstalled = false; + // DON'T install facade - leave window.nostr as the extension + return; + } + + // DEFERRED EXTENSION DETECTION: Extensions like nos2x may load after us + console.log('NOSTR_LOGIN_LITE: No real extension detected initially, starting deferred detection...'); + this.facadeInstalled = false; + + let checkCount = 0; + const maxChecks = 10; // Check for up to 2 seconds + const checkInterval = setInterval(() => { + checkCount++; + const currentNostr = window.nostr; + + console.log('NOSTR_LOGIN_LITE: === DEFERRED CHECK ' + checkCount + '/' + maxChecks + ' ==='); + console.log('NOSTR_LOGIN_LITE: Current window.nostr:', currentNostr); + console.log('NOSTR_LOGIN_LITE: Constructor:', currentNostr?.constructor?.name); + + // Skip if it's our facade + if (currentNostr?.constructor?.name === 'WindowNostr') { + console.log('NOSTR_LOGIN_LITE: Skipping - this is our facade'); + return; + } + + if (this._isRealExtension(currentNostr)) { + console.log('NOSTR_LOGIN_LITE: ✓✓✓ LATE EXTENSION DETECTED - PRESERVING WITHOUT FACADE ✓✓✓'); + console.log('NOSTR_LOGIN_LITE: Extension detected after ' + (checkCount * 200) + 'ms!'); + console.log('NOSTR_LOGIN_LITE: Extension constructor:', currentNostr.constructor?.name); + console.log('NOSTR_LOGIN_LITE: Extension keys:', Object.keys(currentNostr)); + console.log('NOSTR_LOGIN_LITE: Leaving window.nostr untouched for extension compatibility'); + this.preservedExtension = currentNostr; + this.facadeInstalled = false; + clearInterval(checkInterval); + // DON'T install facade - leave window.nostr as the extension + return; + } + + // Stop checking after max attempts - no extension found + if (checkCount >= maxChecks) { + console.log('NOSTR_LOGIN_LITE: ⚠️ MAX CHECKS REACHED - NO EXTENSION FOUND'); + clearInterval(checkInterval); + console.log('NOSTR_LOGIN_LITE: Installing facade for local/NIP-46/readonly methods'); + this._installFacade(); + } + }, 200); // Check every 200ms + + console.log('NOSTR_LOGIN_LITE: Waiting for deferred detection to complete...'); } } + _installFacade(existingNostr = null) { + if (typeof window !== 'undefined' && !this.facadeInstalled) { + console.log('NOSTR_LOGIN_LITE: === _installFacade CALLED ==='); + console.log('NOSTR_LOGIN_LITE: existingNostr parameter:', existingNostr); + console.log('NOSTR_LOGIN_LITE: existingNostr constructor:', existingNostr?.constructor?.name); + console.log('NOSTR_LOGIN_LITE: window.nostr before installation:', window.nostr); + console.log('NOSTR_LOGIN_LITE: window.nostr constructor before:', window.nostr?.constructor?.name); + + const facade = new WindowNostr(this, existingNostr); + window.nostr = facade; + this.facadeInstalled = true; + + console.log('NOSTR_LOGIN_LITE: === FACADE INSTALLED WITH EXTENSION ==='); + console.log('NOSTR_LOGIN_LITE: window.nostr after installation:', window.nostr); + console.log('NOSTR_LOGIN_LITE: window.nostr constructor after:', window.nostr.constructor?.name); + console.log('NOSTR_LOGIN_LITE: facade.existingNostr:', window.nostr.existingNostr); + } + } + + // Helper method to identify real browser extensions + _isRealExtension(obj) { + console.log('NOSTR_LOGIN_LITE: === _isRealExtension DEBUG ==='); + console.log('NOSTR_LOGIN_LITE: obj:', obj); + console.log('NOSTR_LOGIN_LITE: typeof obj:', typeof obj); + + if (!obj || typeof obj !== 'object') { + console.log('NOSTR_LOGIN_LITE: ✗ Not an object'); + return false; + } + + console.log('NOSTR_LOGIN_LITE: Object keys:', Object.keys(obj)); + console.log('NOSTR_LOGIN_LITE: getPublicKey type:', typeof obj.getPublicKey); + console.log('NOSTR_LOGIN_LITE: signEvent type:', typeof obj.signEvent); + + // Must have required Nostr methods + if (typeof obj.getPublicKey !== 'function' || typeof obj.signEvent !== 'function') { + console.log('NOSTR_LOGIN_LITE: ✗ Missing required methods'); + return false; + } + + // Exclude our own library classes + const constructorName = obj.constructor?.name; + console.log('NOSTR_LOGIN_LITE: Constructor name:', constructorName); + + if (constructorName === 'WindowNostr' || constructorName === 'NostrLite') { + console.log('NOSTR_LOGIN_LITE: ✗ Is our library class'); + return false; + } + + // Exclude NostrTools library object + if (obj === window.NostrTools) { + console.log('NOSTR_LOGIN_LITE: ✗ Is NostrTools object'); + return false; + } + + // Real extensions typically have internal properties or specific characteristics + console.log('NOSTR_LOGIN_LITE: Extension property check:'); + console.log(' _isEnabled:', !!obj._isEnabled); + console.log(' enabled:', !!obj.enabled); + console.log(' kind:', !!obj.kind); + console.log(' _eventEmitter:', !!obj._eventEmitter); + console.log(' _scope:', !!obj._scope); + console.log(' _requests:', !!obj._requests); + console.log(' _pubkey:', !!obj._pubkey); + console.log(' name:', !!obj.name); + console.log(' version:', !!obj.version); + console.log(' description:', !!obj.description); + + const hasExtensionProps = !!( + obj._isEnabled || obj.enabled || obj.kind || + obj._eventEmitter || obj._scope || obj._requests || obj._pubkey || + obj.name || obj.version || obj.description + ); + + console.log('NOSTR_LOGIN_LITE: Extension detection result for', constructorName, ':', hasExtensionProps); + return hasExtensionProps; + } + launch(startScreen = 'login') { console.log('NOSTR_LOGIN_LITE: Launching with screen:', startScreen); diff --git a/relay.pid b/relay.pid index 51e6afa..a31b305 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -3327716 +134045 diff --git a/src/config.c b/src/config.c index 624fca3..460f10d 100644 --- a/src/config.c +++ b/src/config.c @@ -18,12 +18,57 @@ extern sqlite3* g_db; config_manager_t g_config_manager = {0}; char g_database_path[512] = {0}; +// ================================ +// NEW ADMIN API STRUCTURES +// ================================ + +// Migration state management +typedef enum { + MIGRATION_NOT_NEEDED, + MIGRATION_NEEDED, + MIGRATION_IN_PROGRESS, + MIGRATION_COMPLETED, + MIGRATION_FAILED +} migration_state_t; + +typedef struct { + migration_state_t state; + int event_config_count; + int table_config_count; + int migration_errors; + time_t migration_started; + time_t migration_completed; + char error_message[512]; +} migration_status_t; + +static migration_status_t g_migration_status = {0}; + +// Configuration source type +typedef enum { + CONFIG_SOURCE_EVENT, // Current event-based system + CONFIG_SOURCE_TABLE, // New table-based system + CONFIG_SOURCE_HYBRID // During migration +} config_source_t; + // Logging functions (defined in main.c) extern void log_info(const char* message); extern void log_success(const char* message); extern void log_warning(const char* message); extern void log_error(const char* message); +// Forward declarations for new admin API functions +int populate_default_config_values(void); +int process_admin_config_event(cJSON* event, char* error_message, size_t error_size); +int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size); +void invalidate_config_cache(void); +int add_auth_rule_from_config(const char* rule_type, const char* pattern_type, + const char* pattern_value, const char* action); +int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type, + const char* pattern_value); +int is_config_table_ready(void); +int migrate_config_from_events_to_table(void); +int populate_config_table_from_event(const cJSON* event); + // Current configuration cache static cJSON* g_current_config = NULL; @@ -811,12 +856,12 @@ int first_time_startup_sequence(const cli_options_t* cli_options) { return -1; } - // 7. Try to store configuration event in database, but cache it if database isn't ready - if (store_config_event_in_database(config_event) == 0) { - log_success("Initial configuration event stored successfully"); + // 7. Process configuration through admin API instead of storing in events table + if (process_startup_config_event_with_fallback(config_event) == 0) { + log_success("Initial configuration processed successfully through admin API"); } else { - log_warning("Failed to store initial configuration event - will retry after database init"); - // Cache the event for later storage + log_warning("Failed to process initial configuration - will retry after database init"); + // Cache the event for later processing if (g_pending_config_event) { cJSON_Delete(g_pending_config_event); } @@ -873,6 +918,9 @@ int startup_existing_relay(const char* relay_pubkey) { g_database_path[sizeof(g_database_path) - 1] = '\0'; free(db_name); + // Configuration will be migrated from events to table after database initialization + log_info("Configuration migration will be performed after database is available"); + // Load configuration event from database (after database is initialized) // This will be done in apply_configuration_from_database() @@ -1579,6 +1627,490 @@ int handle_configuration_event(cJSON* event, char* error_message, size_t error_s } } +// ================================ +// NEW ADMIN API IMPLEMENTATION +// ================================ + +// ================================ +// CONFIG TABLE MANAGEMENT FUNCTIONS +// ================================ + +// Note: Config table is now created via embedded schema in sql_schema.h + +// Get value from config table +const char* get_config_value_from_table(const char* key) { + if (!g_db || !key) return NULL; + + const char* sql = "SELECT value FROM config WHERE key = ?"; + + sqlite3_stmt* stmt; + int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return NULL; + } + + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); + + static char config_value_buffer[CONFIG_VALUE_MAX_LENGTH]; + const char* result = NULL; + + if (sqlite3_step(stmt) == SQLITE_ROW) { + const char* value = (char*)sqlite3_column_text(stmt, 0); + if (value) { + strncpy(config_value_buffer, value, sizeof(config_value_buffer) - 1); + config_value_buffer[sizeof(config_value_buffer) - 1] = '\0'; + result = config_value_buffer; + } + } + + sqlite3_finalize(stmt); + return result; +} + +// Set value in config table +int set_config_value_in_table(const char* key, const char* value, const char* data_type, + const char* description, const char* category, int requires_restart) { + if (!g_db || !key || !value || !data_type) { + return -1; + } + + const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) " + "VALUES (?, ?, ?, ?, ?, ?)"; + + sqlite3_stmt* stmt; + int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return -1; + } + + sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, value, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 4, description ? description : "", -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 5, category ? category : "general", -1, SQLITE_STATIC); + sqlite3_bind_int(stmt, 6, requires_restart); + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? 0 : -1; +} + +// Update config in table (simpler version of set_config_value_in_table) +int update_config_in_table(const char* key, const char* value) { + if (!g_db || !key || !value) { + return -1; + } + + const char* sql = "UPDATE config SET value = ?, updated_at = strftime('%s', 'now') WHERE key = ?"; + + sqlite3_stmt* stmt; + int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return -1; + } + + sqlite3_bind_text(stmt, 1, value, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, key, -1, SQLITE_STATIC); + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? 0 : -1; +} + +// Populate default config values +int populate_default_config_values(void) { + log_info("Populating default configuration values in table..."); + + // Add all default configuration values to the table + for (size_t i = 0; i < DEFAULT_CONFIG_COUNT; i++) { + const char* key = DEFAULT_CONFIG_VALUES[i].key; + const char* value = DEFAULT_CONFIG_VALUES[i].value; + + // Determine data type + const char* data_type = "string"; + if (strcmp(key, "relay_port") == 0 || + strcmp(key, "max_connections") == 0 || + strcmp(key, "pow_min_difficulty") == 0 || + strcmp(key, "max_subscriptions_per_client") == 0 || + strcmp(key, "max_total_subscriptions") == 0 || + strcmp(key, "max_filters_per_subscription") == 0 || + strcmp(key, "max_event_tags") == 0 || + strcmp(key, "max_content_length") == 0 || + strcmp(key, "max_message_length") == 0 || + strcmp(key, "default_limit") == 0 || + strcmp(key, "max_limit") == 0 || + strcmp(key, "nip42_challenge_expiration") == 0 || + strcmp(key, "nip40_expiration_grace_period") == 0) { + data_type = "integer"; + } else if (strcmp(key, "auth_enabled") == 0 || + strcmp(key, "nip40_expiration_enabled") == 0 || + strcmp(key, "nip40_expiration_strict") == 0 || + strcmp(key, "nip40_expiration_filter") == 0 || + strcmp(key, "nip42_auth_required") == 0) { + data_type = "boolean"; + } + + // Set category + const char* category = "general"; + if (strstr(key, "relay_")) { + category = "relay"; + } else if (strstr(key, "nip40_")) { + category = "expiration"; + } else if (strstr(key, "nip42_") || strstr(key, "auth_")) { + category = "authentication"; + } else if (strstr(key, "pow_")) { + category = "proof_of_work"; + } else if (strstr(key, "max_")) { + category = "limits"; + } + + // Determine if requires restart + int requires_restart = 0; + if (strcmp(key, "relay_port") == 0) { + requires_restart = 1; + } + + if (set_config_value_in_table(key, value, data_type, NULL, category, requires_restart) != 0) { + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), "Failed to set default config: %s = %s", key, value); + log_error(error_msg); + } + } + + log_success("Default configuration values populated"); + return 0; +} + +// ================================ +// ADMIN EVENT PROCESSING FUNCTIONS +// ================================ + +// Process admin events (moved from main.c) +int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size) { + cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); + if (!kind_obj || !cJSON_IsNumber(kind_obj)) { + snprintf(error_message, error_size, "invalid: missing or invalid kind"); + return -1; + } + + // Verify admin authorization + cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); + if (!pubkey_obj || !cJSON_IsString(pubkey_obj)) { + snprintf(error_message, error_size, "invalid: missing pubkey"); + return -1; + } + + const char* event_pubkey = cJSON_GetStringValue(pubkey_obj); + const char* admin_pubkey = get_config_value("admin_pubkey"); + + if (!admin_pubkey || strcmp(event_pubkey, admin_pubkey) != 0) { + snprintf(error_message, error_size, "auth-required: not authorized admin"); + return -1; + } + + int kind = (int)cJSON_GetNumberValue(kind_obj); + + switch (kind) { + case 33334: + return process_admin_config_event(event, error_message, error_size); + case 33335: + return process_admin_auth_event(event, error_message, error_size); + default: + snprintf(error_message, error_size, "invalid: unsupported admin event kind"); + return -1; + } +} + +// Handle kind 33334 config events +int process_admin_config_event(cJSON* event, char* error_message, size_t error_size) { + cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); + if (!tags_obj || !cJSON_IsArray(tags_obj)) { + snprintf(error_message, error_size, "invalid: configuration event must have tags"); + return -1; + } + + // Config table should already exist from embedded schema + + // Begin transaction for atomic config updates + int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); + if (rc != SQLITE_OK) { + snprintf(error_message, error_size, "failed to begin config transaction"); + return -1; + } + + int updates_applied = 0; + + // Process each tag as a configuration parameter + cJSON* tag = NULL; + cJSON_ArrayForEach(tag, tags_obj) { + if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { + continue; + } + + cJSON* tag_name = cJSON_GetArrayItem(tag, 0); + cJSON* tag_value = cJSON_GetArrayItem(tag, 1); + + if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) { + continue; + } + + const char* key = cJSON_GetStringValue(tag_name); + const char* value = cJSON_GetStringValue(tag_value); + + // Skip relay identifier tag + if (strcmp(key, "d") == 0) { + continue; + } + + // Update configuration in table + if (update_config_in_table(key, value) == 0) { + updates_applied++; + } + } + + if (updates_applied > 0) { + sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); + invalidate_config_cache(); + + char success_msg[256]; + snprintf(success_msg, sizeof(success_msg), "Applied %d configuration updates", updates_applied); + log_success(success_msg); + } else { + sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + snprintf(error_message, error_size, "no valid configuration parameters found"); + return -1; + } + + return 0; +} + +// Handle kind 33335 auth rule events +int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size) { + cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); + if (!tags_obj || !cJSON_IsArray(tags_obj)) { + snprintf(error_message, error_size, "invalid: auth rule event must have tags"); + return -1; + } + + // Extract action from content or tags + cJSON* content_obj = cJSON_GetObjectItem(event, "content"); + const char* content = content_obj ? cJSON_GetStringValue(content_obj) : ""; + + // Parse the action from content (should be "add" or "remove") + cJSON* content_json = cJSON_Parse(content); + const char* action = "add"; // default + if (content_json) { + cJSON* action_obj = cJSON_GetObjectItem(content_json, "action"); + if (action_obj && cJSON_IsString(action_obj)) { + action = cJSON_GetStringValue(action_obj); + } + cJSON_Delete(content_json); + } + + // Begin transaction for atomic auth rule updates + int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); + if (rc != SQLITE_OK) { + snprintf(error_message, error_size, "failed to begin auth rule transaction"); + return -1; + } + + int rules_processed = 0; + + // Process each tag as an auth rule specification + cJSON* tag = NULL; + cJSON_ArrayForEach(tag, tags_obj) { + if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 3) { + continue; + } + + cJSON* rule_type_obj = cJSON_GetArrayItem(tag, 0); + cJSON* pattern_type_obj = cJSON_GetArrayItem(tag, 1); + cJSON* pattern_value_obj = cJSON_GetArrayItem(tag, 2); + + if (!cJSON_IsString(rule_type_obj) || + !cJSON_IsString(pattern_type_obj) || + !cJSON_IsString(pattern_value_obj)) { + continue; + } + + const char* rule_type = cJSON_GetStringValue(rule_type_obj); + const char* pattern_type = cJSON_GetStringValue(pattern_type_obj); + const char* pattern_value = cJSON_GetStringValue(pattern_value_obj); + + // Process the auth rule based on action + if (strcmp(action, "add") == 0) { + if (add_auth_rule_from_config(rule_type, pattern_type, pattern_value, "allow") == 0) { + rules_processed++; + } + } else if (strcmp(action, "remove") == 0) { + if (remove_auth_rule_from_config(rule_type, pattern_type, pattern_value) == 0) { + rules_processed++; + } + } + } + + if (rules_processed > 0) { + sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); + + char success_msg[256]; + snprintf(success_msg, sizeof(success_msg), "Processed %d auth rule updates", rules_processed); + log_success(success_msg); + } else { + sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + snprintf(error_message, error_size, "no valid auth rules found"); + return -1; + } + + return 0; +} + +// ================================ +// AUTH RULES MANAGEMENT FUNCTIONS +// ================================ + +// Add auth rule from configuration +int add_auth_rule_from_config(const char* rule_type, const char* pattern_type, + const char* pattern_value, const char* action) { + if (!g_db || !rule_type || !pattern_type || !pattern_value || !action) { + return -1; + } + + const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value, action) " + "VALUES (?, ?, ?, ?)"; + + sqlite3_stmt* stmt; + int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return -1; + } + + sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 4, action, -1, SQLITE_STATIC); + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? 0 : -1; +} + +// Remove auth rule from configuration +int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type, + const char* pattern_value) { + if (!g_db || !rule_type || !pattern_type || !pattern_value) { + return -1; + } + + const char* sql = "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?"; + + sqlite3_stmt* stmt; + int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return -1; + } + + sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, pattern_value, -1, SQLITE_STATIC); + + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + return (rc == SQLITE_DONE) ? 0 : -1; +} + +// ================================ +// CONFIGURATION CACHE MANAGEMENT +// ================================ + +// Invalidate configuration cache +void invalidate_config_cache(void) { + // For now, just log that cache was invalidated + // In a full implementation, this would clear any cached config values + log_info("Configuration cache invalidated"); +} + +// Reload configuration from table +int reload_config_from_table(void) { + // For now, just log that config was reloaded + // In a full implementation, this would reload all cached values from the table + log_info("Configuration reloaded from table"); + return 0; +} + +// ================================ +// HYBRID CONFIG ACCESS FUNCTIONS +// ================================ + +// Hybrid config getter (tries table first, falls back to event) +const char* get_config_value_hybrid(const char* key) { + // Try table-based config first if available + if (is_config_table_ready()) { + const char* table_value = get_config_value_from_table(key); + if (table_value) { + return table_value; + } + } + + // Fall back to event-based config + return get_config_value(key); +} + +// Check if config table is ready +int is_config_table_ready(void) { + if (!g_db) return 0; + + const char* sql = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='config'"; + sqlite3_stmt* stmt; + + int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return 0; + } + + int table_exists = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) { + table_exists = sqlite3_column_int(stmt, 0) > 0; + } + sqlite3_finalize(stmt); + + if (!table_exists) { + return 0; + } + + // Check if table has configuration data + const char* count_sql = "SELECT COUNT(*) FROM config"; + rc = sqlite3_prepare_v2(g_db, count_sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + return 0; + } + + int config_count = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) { + config_count = sqlite3_column_int(stmt, 0); + } + sqlite3_finalize(stmt); + + return config_count > 0; +} + +// Initialize configuration system with migration support +int initialize_config_system_with_migration(void) { + log_info("Initializing configuration system with migration support..."); + + // Initialize config manager + memset(&g_config_manager, 0, sizeof(g_config_manager)); + memset(&g_migration_status, 0, sizeof(g_migration_status)); + + // For new installations, config table should already exist from embedded schema + log_success("Configuration system initialized with table support"); + + return 0; +} + // ================================ // RETRY INITIAL CONFIG EVENT STORAGE // ================================ @@ -1591,16 +2123,263 @@ int retry_store_initial_config_event(void) { log_info("Retrying storage of initial configuration event..."); - // Try to store the cached configuration event - if (store_config_event_in_database(g_pending_config_event) == 0) { - log_success("Initial configuration event stored successfully on retry"); + // Try to process the cached configuration event through admin API + if (process_startup_config_event_with_fallback(g_pending_config_event) == 0) { + log_success("Initial configuration processed successfully through admin API on retry"); // Clean up the pending event cJSON_Delete(g_pending_config_event); g_pending_config_event = NULL; return 0; } else { - log_error("Failed to store initial configuration event on retry"); + log_error("Failed to process initial configuration through admin API on retry"); return -1; } +} + +// ================================ +// CONFIG MIGRATION FUNCTIONS +// ================================ + +// Populate config table from a configuration event +int populate_config_table_from_event(const cJSON* event) { + if (!event || !g_db) { + return -1; + } + + log_info("Populating config table from configuration event..."); + + cJSON* tags = cJSON_GetObjectItem(event, "tags"); + if (!tags || !cJSON_IsArray(tags)) { + log_error("Configuration event missing tags array"); + return -1; + } + + int configs_populated = 0; + + // Process each tag as a configuration parameter + cJSON* tag = NULL; + cJSON_ArrayForEach(tag, tags) { + if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { + continue; + } + + cJSON* tag_name = cJSON_GetArrayItem(tag, 0); + cJSON* tag_value = cJSON_GetArrayItem(tag, 1); + + if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) { + continue; + } + + const char* key = cJSON_GetStringValue(tag_name); + const char* value = cJSON_GetStringValue(tag_value); + + // Skip relay identifier tag + if (strcmp(key, "d") == 0) { + continue; + } + + // Determine data type for the config value + const char* data_type = "string"; + if (strcmp(key, "relay_port") == 0 || + strcmp(key, "max_connections") == 0 || + strcmp(key, "pow_min_difficulty") == 0 || + strcmp(key, "max_subscriptions_per_client") == 0 || + strcmp(key, "max_total_subscriptions") == 0 || + strcmp(key, "max_filters_per_subscription") == 0 || + strcmp(key, "max_event_tags") == 0 || + strcmp(key, "max_content_length") == 0 || + strcmp(key, "max_message_length") == 0 || + strcmp(key, "default_limit") == 0 || + strcmp(key, "max_limit") == 0 || + strcmp(key, "nip42_challenge_expiration") == 0 || + strcmp(key, "nip40_expiration_grace_period") == 0) { + data_type = "integer"; + } else if (strcmp(key, "auth_enabled") == 0 || + strcmp(key, "nip40_expiration_enabled") == 0 || + strcmp(key, "nip40_expiration_strict") == 0 || + strcmp(key, "nip40_expiration_filter") == 0 || + strcmp(key, "nip42_auth_required") == 0) { + data_type = "boolean"; + } + + // Set category + const char* category = "general"; + if (strstr(key, "relay_")) { + category = "relay"; + } else if (strstr(key, "nip40_")) { + category = "expiration"; + } else if (strstr(key, "nip42_") || strstr(key, "auth_")) { + category = "authentication"; + } else if (strstr(key, "pow_")) { + category = "proof_of_work"; + } else if (strstr(key, "max_")) { + category = "limits"; + } + + // Determine if requires restart + int requires_restart = 0; + if (strcmp(key, "relay_port") == 0) { + requires_restart = 1; + } + + // Insert into config table + if (set_config_value_in_table(key, value, data_type, NULL, category, requires_restart) == 0) { + configs_populated++; + } else { + char error_msg[256]; + snprintf(error_msg, sizeof(error_msg), "Failed to populate config: %s = %s", key, value); + log_error(error_msg); + } + } + + if (configs_populated > 0) { + char success_msg[256]; + snprintf(success_msg, sizeof(success_msg), "Populated %d configuration values from event", configs_populated); + log_success(success_msg); + return 0; + } else { + log_error("No configuration values were populated from event"); + return -1; + } +} + +// Migrate configuration from existing events to config table +int migrate_config_from_events_to_table(void) { + if (!g_db) { + log_error("Database not available for configuration migration"); + return -1; + } + + log_info("Migrating configuration from events to config table..."); + + // Load the most recent configuration event from database + cJSON* config_event = load_config_event_from_database(g_config_manager.relay_pubkey); + if (!config_event) { + log_info("No existing configuration event found - migration not needed"); + return 0; + } + + // Populate config table from the event + int result = populate_config_table_from_event(config_event); + + // Clean up + cJSON_Delete(config_event); + + if (result == 0) { + log_success("Configuration migration from events to table completed successfully"); + } else { + log_error("Configuration migration from events to table failed"); + } + + return result; +} + +// ================================ +// STARTUP CONFIGURATION PROCESSING +// ================================ + +// Process startup configuration event - bypasses auth and updates config table +int process_startup_config_event(const cJSON* event) { + if (!event || !g_db) { + log_error("Invalid parameters for startup config processing"); + return -1; + } + + log_info("Processing startup configuration event through admin API..."); + + // Validate event structure first + cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); + if (!kind_obj || cJSON_GetNumberValue(kind_obj) != 33334) { + log_error("Invalid event kind for startup configuration"); + return -1; + } + + cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); + if (!tags_obj || !cJSON_IsArray(tags_obj)) { + log_error("Startup configuration event missing tags"); + return -1; + } + + // Begin transaction for atomic config updates + int rc = sqlite3_exec(g_db, "BEGIN IMMEDIATE TRANSACTION", NULL, NULL, NULL); + if (rc != SQLITE_OK) { + log_error("Failed to begin startup config transaction"); + return -1; + } + + int updates_applied = 0; + + // Process each tag as a configuration parameter (same logic as process_admin_config_event) + cJSON* tag = NULL; + cJSON_ArrayForEach(tag, tags_obj) { + if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { + continue; + } + + cJSON* tag_name = cJSON_GetArrayItem(tag, 0); + cJSON* tag_value = cJSON_GetArrayItem(tag, 1); + + if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) { + continue; + } + + const char* key = cJSON_GetStringValue(tag_name); + const char* value = cJSON_GetStringValue(tag_value); + + // Skip relay identifier tag and relay_pubkey (already in table) + if (strcmp(key, "d") == 0 || strcmp(key, "relay_pubkey") == 0) { + continue; + } + + // Update configuration in table + if (update_config_in_table(key, value) == 0) { + updates_applied++; + } + } + + if (updates_applied > 0) { + sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL); + invalidate_config_cache(); + + char success_msg[256]; + snprintf(success_msg, sizeof(success_msg), + "Processed startup configuration: %d values updated in config table", updates_applied); + log_success(success_msg); + return 0; + } else { + sqlite3_exec(g_db, "ROLLBACK", NULL, NULL, NULL); + log_error("No valid configuration parameters found in startup event"); + return -1; + } +} + +// Process startup configuration event with fallback - for retry scenarios +int process_startup_config_event_with_fallback(const cJSON* event) { + if (!event) { + log_error("Invalid event for startup config processing with fallback"); + return -1; + } + + // Try to process through admin API first + if (process_startup_config_event(event) == 0) { + log_success("Startup configuration processed successfully through admin API"); + return 0; + } + + // If that fails, populate defaults and try again + log_warning("Startup config processing failed - ensuring defaults are populated"); + if (populate_default_config_values() != 0) { + log_error("Failed to populate default config values"); + return -1; + } + + // Retry processing + if (process_startup_config_event(event) == 0) { + log_success("Startup configuration processed successfully after populating defaults"); + return 0; + } + + log_error("Startup configuration processing failed even after populating defaults"); + return -1; } \ No newline at end of file diff --git a/src/config.h b/src/config.h index c1393e7..a1f3df6 100644 --- a/src/config.h +++ b/src/config.h @@ -90,4 +90,43 @@ int parse_auth_required_kinds(const char* kinds_str, int* kinds_array, int max_k int is_nip42_auth_required_for_kind(int event_kind); int is_nip42_auth_globally_required(void); +// ================================ +// NEW ADMIN API FUNCTIONS +// ================================ + +// Config table management functions (config table created via embedded schema) +const char* get_config_value_from_table(const char* key); +int set_config_value_in_table(const char* key, const char* value, const char* data_type, + const char* description, const char* category, int requires_restart); +int update_config_in_table(const char* key, const char* value); +int populate_default_config_values(void); + +// Admin event processing functions +int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size); +int process_admin_config_event(cJSON* event, char* error_message, size_t error_size); +int process_admin_auth_event(cJSON* event, char* error_message, size_t error_size); + +// Auth rules management functions +int add_auth_rule_from_config(const char* rule_type, const char* pattern_type, + const char* pattern_value, const char* action); +int remove_auth_rule_from_config(const char* rule_type, const char* pattern_type, + const char* pattern_value); + +// Configuration cache management +void invalidate_config_cache(void); +int reload_config_from_table(void); + +// Hybrid config access functions +const char* get_config_value_hybrid(const char* key); +int is_config_table_ready(void); + +// Migration support functions +int initialize_config_system_with_migration(void); +int migrate_config_from_events_to_table(void); +int populate_config_table_from_event(const cJSON* event); + +// Startup configuration processing functions +int process_startup_config_event(const cJSON* event); +int process_startup_config_event_with_fallback(const cJSON* event); + #endif /* CONFIG_H */ \ No newline at end of file diff --git a/src/main.c b/src/main.c index 3740af1..091ec3d 100644 --- a/src/main.c +++ b/src/main.c @@ -227,6 +227,9 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length); // Forward declaration for configuration event handling (kind 33334) int handle_configuration_event(cJSON* event, char* error_message, size_t error_size); +// Forward declaration for admin event processing (kinds 33334 and 33335) +int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size); + // Forward declaration for NOTICE message support void send_notice_message(struct lws* wsi, const char* message); @@ -3092,17 +3095,47 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso // Cleanup event JSON string free(event_json_str); - // Store event in database and broadcast to subscriptions + // Check for admin events (kinds 33334 and 33335) and intercept them if (result == 0) { - // Store the event in the database first - if (store_event(event) != 0) { - log_error("Failed to store event in database"); - result = -1; - strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1); + cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); + if (kind_obj && cJSON_IsNumber(kind_obj)) { + int event_kind = (int)cJSON_GetNumberValue(kind_obj); + + if (event_kind == 33334 || event_kind == 33335) { + // This is an admin event - process it through the admin API instead of normal storage + log_info("Admin event detected, processing through admin API"); + + char admin_error[512] = {0}; + if (process_admin_event_in_config(event, admin_error, sizeof(admin_error)) != 0) { + log_error("Failed to process admin event through admin API"); + result = -1; + strncpy(error_message, admin_error, sizeof(error_message) - 1); + } else { + log_success("Admin event processed successfully through admin API"); + // Admin events are processed by the admin API, not broadcast to subscriptions + } + } else { + // Regular event - store in database and broadcast + if (store_event(event) != 0) { + log_error("Failed to store event in database"); + result = -1; + strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1); + } else { + log_info("Event stored successfully in database"); + // Broadcast event to matching persistent subscriptions + broadcast_event_to_subscriptions(event); + } + } } else { - log_info("Event stored successfully in database"); - // Broadcast event to matching persistent subscriptions - broadcast_event_to_subscriptions(event); + // Event without valid kind - try normal storage + if (store_event(event) != 0) { + log_error("Failed to store event in database"); + result = -1; + strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1); + } else { + log_info("Event stored successfully in database"); + broadcast_event_to_subscriptions(event); + } } } diff --git a/src/sql_schema.h b/src/sql_schema.h index 1af6636..19acdb8 100644 --- a/src/sql_schema.h +++ b/src/sql_schema.h @@ -1,12 +1,12 @@ /* Embedded SQL Schema for C Nostr Relay * Generated from db/schema.sql - Do not edit manually - * Schema Version: 6 + * Schema Version: 7 */ #ifndef SQL_SCHEMA_H #define SQL_SCHEMA_H /* Schema version constant */ -#define EMBEDDED_SCHEMA_VERSION "6" +#define EMBEDDED_SCHEMA_VERSION "7" /* Embedded SQL schema as C string literal */ static const char* const EMBEDDED_SCHEMA_SQL = @@ -15,7 +15,7 @@ static const char* const EMBEDDED_SCHEMA_SQL = -- Event-based configuration system using kind 33334 Nostr events\n\ \n\ -- Schema version tracking\n\ -PRAGMA user_version = 6;\n\ +PRAGMA user_version = 7;\n\ \n\ -- Enable foreign key support\n\ PRAGMA foreign_keys = ON;\n\ @@ -58,8 +58,8 @@ CREATE TABLE schema_info (\n\ \n\ -- Insert schema metadata\n\ INSERT INTO schema_info (key, value) VALUES\n\ - ('version', '6'),\n\ - ('description', 'Event-based Nostr relay schema with secure relay private key storage'),\n\ + ('version', '7'),\n\ + ('description', 'Hybrid Nostr relay schema with event-based and table-based configuration'),\n\ ('created_at', strftime('%s', 'now'));\n\ \n\ -- Helper views for common queries\n\ @@ -154,6 +154,60 @@ CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value);\ CREATE INDEX idx_auth_rules_type ON auth_rules(rule_type);\n\ CREATE INDEX idx_auth_rules_active ON auth_rules(active);\n\ \n\ +-- Configuration Table for Table-Based Config Management\n\ +-- Hybrid system supporting both event-based and table-based configuration\n\ +CREATE TABLE config (\n\ + key TEXT PRIMARY KEY,\n\ + value TEXT NOT NULL,\n\ + data_type TEXT NOT NULL CHECK (data_type IN ('string', 'integer', 'boolean', 'json')),\n\ + description TEXT,\n\ + category TEXT DEFAULT 'general',\n\ + requires_restart INTEGER DEFAULT 0,\n\ + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),\n\ + updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))\n\ +);\n\ +\n\ +-- Indexes for config table performance\n\ +CREATE INDEX idx_config_category ON config(category);\n\ +CREATE INDEX idx_config_restart ON config(requires_restart);\n\ +CREATE INDEX idx_config_updated ON config(updated_at DESC);\n\ +\n\ +-- Trigger to update config timestamp on changes\n\ +CREATE TRIGGER update_config_timestamp\n\ + AFTER UPDATE ON config\n\ + FOR EACH ROW\n\ +BEGIN\n\ + UPDATE config SET updated_at = strftime('%s', 'now') WHERE key = NEW.key;\n\ +END;\n\ +\n\ +-- Insert default configuration values\n\ +INSERT INTO config (key, value, data_type, description, category, requires_restart) VALUES\n\ + ('relay_description', 'A C Nostr Relay', 'string', 'Relay description', 'general', 0),\n\ + ('relay_contact', '', 'string', 'Relay contact information', 'general', 0),\n\ + ('relay_software', 'https://github.com/laanwj/c-relay', 'string', 'Relay software URL', 'general', 0),\n\ + ('relay_version', '1.0.0', 'string', 'Relay version', 'general', 0),\n\ + ('relay_port', '8888', 'integer', 'Relay port number', 'network', 1),\n\ + ('max_connections', '1000', 'integer', 'Maximum concurrent connections', 'network', 1),\n\ + ('auth_enabled', 'false', 'boolean', 'Enable NIP-42 authentication', 'auth', 0),\n\ + ('nip42_auth_required_events', 'false', 'boolean', 'Require auth for event publishing', 'auth', 0),\n\ + ('nip42_auth_required_subscriptions', 'false', 'boolean', 'Require auth for subscriptions', 'auth', 0),\n\ + ('nip42_auth_required_kinds', '[]', 'json', 'Event kinds requiring authentication', 'auth', 0),\n\ + ('nip42_challenge_expiration', '600', 'integer', 'Auth challenge expiration seconds', 'auth', 0),\n\ + ('pow_min_difficulty', '0', 'integer', 'Minimum proof-of-work difficulty', 'validation', 0),\n\ + ('pow_mode', 'optional', 'string', 'Proof-of-work mode', 'validation', 0),\n\ + ('nip40_expiration_enabled', 'true', 'boolean', 'Enable event expiration', 'validation', 0),\n\ + ('nip40_expiration_strict', 'false', 'boolean', 'Strict expiration mode', 'validation', 0),\n\ + ('nip40_expiration_filter', 'true', 'boolean', 'Filter expired events in queries', 'validation', 0),\n\ + ('nip40_expiration_grace_period', '60', 'integer', 'Expiration grace period seconds', 'validation', 0),\n\ + ('max_subscriptions_per_client', '25', 'integer', 'Maximum subscriptions per client', 'limits', 0),\n\ + ('max_total_subscriptions', '1000', 'integer', 'Maximum total subscriptions', 'limits', 0),\n\ + ('max_filters_per_subscription', '10', 'integer', 'Maximum filters per subscription', 'limits', 0),\n\ + ('max_event_tags', '2000', 'integer', 'Maximum tags per event', 'limits', 0),\n\ + ('max_content_length', '100000', 'integer', 'Maximum event content length', 'limits', 0),\n\ + ('max_message_length', '131072', 'integer', 'Maximum WebSocket message length', 'limits', 0),\n\ + ('default_limit', '100', 'integer', 'Default query limit', 'limits', 0),\n\ + ('max_limit', '5000', 'integer', 'Maximum query limit', 'limits', 0);\n\ +\n\ -- Persistent Subscriptions Logging Tables (Phase 2)\n\ -- Optional database logging for subscription analytics and debugging\n\ \n\ diff --git a/test_relay.js b/test_relay.js deleted file mode 100644 index bfe920a..0000000 --- a/test_relay.js +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env node - -// Import the nostr-tools bundle -const fs = require('fs'); -const path = require('path'); -const { TextEncoder, TextDecoder } = require('util'); - -// Load nostr.bundle.js -const bundlePath = path.join(__dirname, 'api', 'nostr.bundle.js'); -if (!fs.existsSync(bundlePath)) { - console.error('nostr.bundle.js not found at:', bundlePath); - process.exit(1); -} - -// Read and eval the bundle to get NostrTools -const bundleCode = fs.readFileSync(bundlePath, 'utf8'); -const vm = require('vm'); - -// Create a more complete browser-like context -const context = { - window: {}, - global: {}, - console: console, - setTimeout: setTimeout, - setInterval: setInterval, - clearTimeout: clearTimeout, - clearInterval: clearInterval, - Buffer: Buffer, - process: process, - require: require, - module: module, - exports: exports, - __dirname: __dirname, - __filename: __filename, - TextEncoder: TextEncoder, - TextDecoder: TextDecoder, - crypto: require('crypto'), - atob: (str) => Buffer.from(str, 'base64').toString('binary'), - btoa: (str) => Buffer.from(str, 'binary').toString('base64'), - fetch: require('https').get // Basic polyfill, might need adjustment -}; - -// Add common browser globals to window -context.window.TextEncoder = TextEncoder; -context.window.TextDecoder = TextDecoder; -context.window.crypto = context.crypto; -context.window.atob = context.atob; -context.window.btoa = context.btoa; -context.window.console = console; -context.window.setTimeout = setTimeout; -context.window.setInterval = setInterval; -context.window.clearTimeout = clearTimeout; -context.window.clearInterval = clearInterval; - -// Execute bundle in context -vm.createContext(context); -try { - vm.runInContext(bundleCode, context); -} catch (error) { - console.error('Error loading nostr bundle:', error.message); - process.exit(1); -} - -// Debug what's available in the context -console.log('Bundle loaded, checking available objects...'); -console.log('context.window keys:', Object.keys(context.window)); -console.log('context.global keys:', Object.keys(context.global)); - -// Try different ways to access NostrTools -let NostrTools = context.window.NostrTools || context.NostrTools || context.global.NostrTools; - -// If still not found, look for other possible exports -if (!NostrTools) { - console.log('Looking for alternative exports...'); - - // Check if it's under a different name - const windowKeys = Object.keys(context.window); - const possibleExports = windowKeys.filter(key => - key.toLowerCase().includes('nostr') || - key.toLowerCase().includes('tools') || - typeof context.window[key] === 'object' - ); - - console.log('Possible nostr-related exports:', possibleExports); - - // Try the first one that looks promising - if (possibleExports.length > 0) { - NostrTools = context.window[possibleExports[0]]; - console.log(`Trying ${possibleExports[0]}:`, typeof NostrTools); - } -} - -if (!NostrTools) { - console.error('NostrTools not found in bundle'); - console.error('Bundle might not be compatible with Node.js or needs different loading approach'); - process.exit(1); -} - -console.log('NostrTools loaded successfully'); -console.log('Available methods:', Object.keys(NostrTools)); - -async function testRelay() { - const relayUrl = 'ws://127.0.0.1:8888'; - - try { - console.log('\n=== Testing Relay Connection ==='); - console.log('Relay URL:', relayUrl); - - // Create SimplePool - const pool = new NostrTools.SimplePool(); - console.log('SimplePool created'); - - // Test 1: Query for kind 1 events - console.log('\n--- Test 1: Kind 1 Events ---'); - const kind1Events = await pool.querySync([relayUrl], { - kinds: [1], - limit: 5 - }); - - console.log(`Found ${kind1Events.length} kind 1 events`); - kind1Events.forEach((event, index) => { - console.log(`Event ${index + 1}:`, { - id: event.id, - kind: event.kind, - pubkey: event.pubkey.substring(0, 16) + '...', - created_at: new Date(event.created_at * 1000).toISOString(), - content: event.content.substring(0, 50) + (event.content.length > 50 ? '...' : '') - }); - }); - - // Test 2: Query for kind 33334 events (configuration) - console.log('\n--- Test 2: Kind 33334 Events (Configuration) ---'); - const configEvents = await pool.querySync([relayUrl], { - kinds: [33334], - limit: 10 - }); - - console.log(`Found ${configEvents.length} kind 33334 events`); - configEvents.forEach((event, index) => { - console.log(`Config Event ${index + 1}:`, { - id: event.id, - kind: event.kind, - pubkey: event.pubkey.substring(0, 16) + '...', - created_at: new Date(event.created_at * 1000).toISOString(), - tags: event.tags.length, - content: event.content - }); - - // Show some tags - if (event.tags.length > 0) { - console.log(' Sample tags:'); - event.tags.slice(0, 5).forEach(tag => { - console.log(` ${tag[0]}: ${tag[1] || ''}`); - }); - } - }); - - // Test 3: Query for any events - console.log('\n--- Test 3: Any Events (limit 3) ---'); - const anyEvents = await pool.querySync([relayUrl], { - limit: 3 - }); - - console.log(`Found ${anyEvents.length} total events`); - anyEvents.forEach((event, index) => { - console.log(`Event ${index + 1}:`, { - id: event.id, - kind: event.kind, - pubkey: event.pubkey.substring(0, 16) + '...', - created_at: new Date(event.created_at * 1000).toISOString() - }); - }); - - // Clean up - pool.close([relayUrl]); - console.log('\n=== Test Complete ==='); - - } catch (error) { - console.error('Relay test failed:', error.message); - console.error('Stack:', error.stack); - } -} - -// Run the test -testRelay().then(() => { - console.log('Test finished'); - process.exit(0); -}).catch((error) => { - console.error('Test failed:', error); - process.exit(1); -}); \ No newline at end of file