diff --git a/07.md b/07.md deleted file mode 100644 index 294acd3..0000000 --- a/07.md +++ /dev/null @@ -1,32 +0,0 @@ -NIP-07 -====== - -`window.nostr` capability for web browsers ------------------------------------------- - -`draft` `optional` - -The `window.nostr` object may be made available by web browsers or extensions and websites or web-apps may make use of it after checking its availability. - -That object must define the following methods: - -``` -async window.nostr.getPublicKey(): string // returns a public key as hex -async window.nostr.signEvent(event: { created_at: number, kind: number, tags: string[][], content: string }): Event // takes an event object, adds `id`, `pubkey` and `sig` and returns it -``` - -Aside from these two basic above, the following functions can also be implemented optionally: -``` -async window.nostr.nip04.encrypt(pubkey, plaintext): string // returns ciphertext and iv as specified in nip-04 (deprecated) -async window.nostr.nip04.decrypt(pubkey, ciphertext): string // takes ciphertext and iv as specified in nip-04 (deprecated) -async window.nostr.nip44.encrypt(pubkey, plaintext): string // returns ciphertext as specified in nip-44 -async window.nostr.nip44.decrypt(pubkey, ciphertext): string // takes ciphertext as specified in nip-44 -``` - -### Recommendation to Extension Authors -To make sure that the `window.nostr` is available to nostr clients on page load, the authors who create Chromium and Firefox extensions should load their scripts by specifying `"run_at": "document_end"` in the extension's manifest. - - -### Implementation - -See https://github.com/aljazceru/awesome-nostr#nip-07-browser-extensions. diff --git a/FIX_ME.md b/FIX_ME.md new file mode 100644 index 0000000..e5b72eb --- /dev/null +++ b/FIX_ME.md @@ -0,0 +1,94 @@ +Inconsistency audit with exact fixes (treating README.md as authoritative) + +Backend auth_rules schema mismatch +Evidence: +Migration (creates mismatched columns): +See "CREATE TABLE IF NOT EXISTS auth_rules ... UNIQUE(rule_type, operation, rule_target)". +Active code uses rule_type, pattern_type, pattern_value, action: +Insert: "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value, action)" +Delete: "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?" +Query mapping: map_auth_query_type_to_response() +Queries: "... WHERE rule_type LIKE '%blacklist%'" +Validator checks: +"... WHERE rule_type = 'blacklist' AND pattern_type = 'pubkey' AND pattern_value = ?" +"... WHERE rule_type = 'blacklist' AND pattern_type = 'hash' AND pattern_value = ?" +"... WHERE rule_type = 'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ?" +Embedded schema expects pattern columns and active/indexes: +"CREATE TABLE auth_rules ( ... )" +"CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value)" +"CREATE INDEX idx_auth_rules_active ON auth_rules(active)" +Fix (update migration to align with sql_schema.h/config.c): +Replace the DDL at "create_auth_rules_sql" with: CREATE TABLE IF NOT EXISTS auth_rules ( id INTEGER PRIMARY KEY AUTOINCREMENT, rule_type TEXT NOT NULL, -- 'whitelist' | 'blacklist' pattern_type TEXT NOT NULL, -- 'pubkey' | 'hash' | future pattern_value TEXT NOT NULL, -- hex pubkey/hash action TEXT NOT NULL, -- 'allow' | 'deny' active INTEGER DEFAULT 1, created_at INTEGER DEFAULT (strftime('%s','now')), UNIQUE(rule_type, pattern_type, pattern_value) ); +After creation, also create indexes as in "sql_schema.h": +CREATE INDEX idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value); +CREATE INDEX idx_auth_rules_type ON auth_rules(rule_type); +CREATE INDEX idx_auth_rules_active ON auth_rules(active); +Duplicate UI function + stale DOM id usage +Evidence: +Duplicate definition of disconnectFromRelay() and disconnectFromRelay(); the second overwrites the first and uses legacy element access paths. +Stale variable: "const relayUrl = document.getElementById('relay-url');" — no element with id="relay-url" exists; the real input is "relay-connection-url" and is referenced as "relayConnectionUrl". +Calls using relayUrl.value.trim() (must use relayConnectionUrl): +"sendConfigUpdateCommand() publish URL" +"loadAuthRules() publish URL" +"deleteAuthRule() publish URL" +Tests: +"testGetAuthRules()" +"testClearAuthRules()" +"testAddBlacklist()" +"testAddWhitelist()" +"testConfigQuery()" +"testPostEvent()" +Fix: +Remove the duplicate legacy function entirely: delete the second disconnectFromRelay(). +Remove stale variable: delete "const relayUrl = document.getElementById('relay-url');". +Replace every relayUrl.value.trim() occurrence with relayConnectionUrl.value.trim() at the lines listed above. +Supported NIPs inconsistency (README vs UI fallback) +Evidence: +README implemented NIPs checklist (authoritative): "NIPs list" shows: 1, 9, 11, 13, 15, 20, 33, 40, 42 implemented. +UI fallback for manual relay info includes unsupported/undocumented NIPs and misses implemented ones: +"supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22]" +"supported_nips: [1, 2, 4, 9, 11, 12, 15, 16, 20, 22]" +Fix: +Replace both arrays with: [1, 9, 11, 13, 15, 20, 33, 40, 42] +Config key mismatches (README vs UI edit form) +Evidence: +README keys (authoritative): "Available Configuration Keys"–(README.md:110) +relay_description, relay_contact, max_connections, max_subscriptions_per_client, max_event_tags, max_content_length, auth_enabled, nip42_auth_required, nip42_auth_required_kinds, nip42_challenge_timeout, pow_min_difficulty, nip40_expiration_enabled +UI currently declares/uses many non-README keys: +Field types: "fieldTypes" include nip42_auth_required_events, nip42_auth_required_subscriptions, relay_port, pow_mode, nip40_expiration_strict, nip40_expiration_filter, nip40_expiration_grace_period, max_total_subscriptions, max_filters_per_subscription, max_message_length, default_limit, max_limit. +Descriptions: "descriptions" reflect the same non-README keys. +Fix: +Restrict UI form generation to README keys and rename mismatches: +Combine nip42_auth_required_events/subscriptions into README’s "nip42_auth_required" (boolean). +Rename nip42_challenge_expiration to "nip42_challenge_timeout". +Remove or hide (advanced section) non-README keys: relay_port, pow_mode, nip40_expiration_strict, nip40_expiration_filter, nip40_expiration_grace_period, max_total_subscriptions, max_filters_per_subscription, max_message_length, default_limit, max_limit. +Update both "fieldTypes" and "descriptions" to reflect only README keys (data types and labels consistent). +First-time startup port override (-p) ignored when -a and -r are also provided +Observation: +You confirmed: first run with -p 7777 works, but with -p plus -a and -r the override isn’t honored. +Likely cause: +The code path that handles admin/relay key overrides on first-time setup bypasses persisting the CLI port override to config/unified cache before server start, so "start_websocket_relay(-1, ...)" falls back to default. +Fix: +Ensure first_time_startup_sequence applies cli_options.port_override to persistent config and cache BEFORE default config insertion and before starting the server. Specifically: +In the first-time path (main): +After "first_time_startup_sequence(&cli_options)" and before creating defaults on the -a/-r path at "populate_default_config_values()", write the port override: +set_config_value_in_table("relay_port", "", "integer", "WebSocket port", "relay", 0); +and update unified cache if required by the port resolution code. +Verify the code path where -a/-r trigger direct table population also applies/overwrites the port with the CLI-provided value. +Add a regression test to assert that -p is honored with and without -a/-r on first run. +Minor consistency recommendations +UI NIP-11 fallback version string: +Consider aligning with backend version source (e.g., src/version.h). The UI currently hardcodes "1.0.0" at "version: '1.0.0'". +UI hardcoded relay pubkey fallback: +"getRelayPubkey()" returns a constant when not connected. Safe for dev, but should not leak into production paths. +Added TODO items (as requested) + +The following todos were added/organized: + Remove duplicate disconnectFromRelay() and standardize to relay-connection-url + Replace all relayUrl.value references with relayConnectionUrl.value in api/index.html + Align Supported NIPs fallback arrays in api/index.html with README (1,9,11,13,15,20,33,40,42) + Update config form keys/descriptions in api/index.html to match README keys + Fix backend auth_rules migration in src/main.c to match src/sql_schema.h/src/config.c + Investigate and fix first-time startup port override ignored when -a and -r are provided + Add tests for port override and auth_rules flows + Rebuild via ./make_and_restart_relay.sh and validate against README diff --git a/make_and_restart_relay.sh b/make_and_restart_relay.sh index 712c9bd..57beac3 100755 --- a/make_and_restart_relay.sh +++ b/make_and_restart_relay.sh @@ -282,7 +282,7 @@ cd build # Start relay in background and capture its PID if [ "$USE_TEST_KEYS" = true ]; then echo "Using deterministic test keys for development..." - ./$(basename $BINARY_PATH) -a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -r 1111111111111111111111111111111111111111111111111111111111111111 --strict-port > ../relay.log 2>&1 & + ./$(basename $BINARY_PATH) -a 6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3 -r 1111111111111111111111111111111111111111111111111111111111111111 --strict-port > ../relay.log 2>&1 & elif [ -n "$RELAY_ARGS" ]; then echo "Starting relay with custom configuration..." ./$(basename $BINARY_PATH) $RELAY_ARGS --strict-port > ../relay.log 2>&1 & diff --git a/relay.pid b/relay.pid index e89f8a7..7ccf87c 100644 --- a/relay.pid +++ b/relay.pid @@ -1 +1 @@ -1487904 +1796483 diff --git a/src/config.c b/src/config.c index 0d17878..f7c69be 100644 --- a/src/config.c +++ b/src/config.c @@ -921,40 +921,46 @@ int first_time_startup_sequence(const cli_options_t* cli_options) { // 1. Generate or use provided admin keypair unsigned char admin_privkey_bytes[32]; char admin_privkey[65], admin_pubkey[65]; - - if (cli_options && strlen(cli_options->admin_privkey_override) == 64) { - // Use provided admin private key - log_info("Using provided admin private key override"); - strncpy(admin_privkey, cli_options->admin_privkey_override, sizeof(admin_privkey) - 1); - admin_privkey[sizeof(admin_privkey) - 1] = '\0'; - - // Convert hex string to bytes - if (nostr_hex_to_bytes(admin_privkey, admin_privkey_bytes, 32) != NOSTR_SUCCESS) { - log_error("Failed to convert admin private key hex to bytes"); - return -1; - } - - // Validate the private key - if (nostr_ec_private_key_verify(admin_privkey_bytes) != NOSTR_SUCCESS) { - log_error("Provided admin private key is invalid"); - return -1; + + if (cli_options && strlen(cli_options->admin_pubkey_override) == 64) { + // Use provided admin public key directly - skip private key generation entirely + log_info("Using provided admin public key override - skipping private key generation"); + strncpy(admin_pubkey, cli_options->admin_pubkey_override, sizeof(admin_pubkey) - 1); + admin_pubkey[sizeof(admin_pubkey) - 1] = '\0'; + + // Validate the public key format (must be 64 hex characters) + for (int i = 0; i < 64; i++) { + char c = admin_pubkey[i]; + if (!((c >= '0' && c <= '9') || + (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'))) { + log_error("Invalid admin public key format - must contain only hex characters"); + return -1; + } } + + // Skip private key generation - we only need the pubkey for admin verification + // Set a dummy private key that will never be used (not displayed or stored) + memset(admin_privkey_bytes, 0, 32); // Zero out for security + memset(admin_privkey, 0, sizeof(admin_privkey)); // Zero out the hex string } else { // Generate random admin keypair using /dev/urandom + nostr_core_lib + log_info("Generating random admin keypair"); if (generate_random_private_key_bytes(admin_privkey_bytes) != 0) { log_error("Failed to generate admin private key"); return -1; } nostr_bytes_to_hex(admin_privkey_bytes, 32, admin_privkey); + + // Derive public key from private key + unsigned char admin_pubkey_bytes[32]; + if (nostr_ec_public_key_from_private_key(admin_privkey_bytes, admin_pubkey_bytes) != NOSTR_SUCCESS) { + log_error("Failed to derive admin public key"); + return -1; + } + nostr_bytes_to_hex(admin_pubkey_bytes, 32, admin_pubkey); } - unsigned char admin_pubkey_bytes[32]; - if (nostr_ec_public_key_from_private_key(admin_privkey_bytes, admin_pubkey_bytes) != NOSTR_SUCCESS) { - log_error("Failed to derive admin public key"); - return -1; - } - nostr_bytes_to_hex(admin_pubkey_bytes, 32, admin_pubkey); - // 2. Generate or use provided relay keypair unsigned char relay_privkey_bytes[32]; char relay_privkey[65], relay_pubkey[65]; @@ -1011,34 +1017,43 @@ int first_time_startup_sequence(const cli_options_t* cli_options) { g_temp_relay_privkey[sizeof(g_temp_relay_privkey) - 1] = '\0'; log_info("Relay private key cached for secure storage after database initialization"); - // 6. Create initial configuration event using defaults (without private key) - cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey, cli_options); - if (!config_event) { - log_error("Failed to create default configuration event"); - return -1; - } - - // 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"); + // 6. Handle configuration setup based on admin key availability + if (cli_options && strlen(cli_options->admin_pubkey_override) == 64) { + // Admin pubkey provided - will populate config table after database initialization + log_info("Admin pubkey provided - config table will be populated after database initialization"); } else { - 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); + // Admin private key available - create signed configuration event + log_info("Admin private key available - creating signed configuration event"); + + // Create initial configuration event using defaults + cJSON* config_event = create_default_config_event(admin_privkey_bytes, relay_privkey, relay_pubkey, cli_options); + if (!config_event) { + log_error("Failed to create default configuration event"); + return -1; } - g_pending_config_event = cJSON_Duplicate(config_event, 1); + + // 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 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); + } + g_pending_config_event = cJSON_Duplicate(config_event, 1); + } + + // Cache the current config + if (g_current_config) { + cJSON_Delete(g_current_config); + } + g_current_config = cJSON_Duplicate(config_event, 1); + + // Clean up + cJSON_Delete(config_event); } - // 8. Cache the current config - if (g_current_config) { - cJSON_Delete(g_current_config); - } - g_current_config = cJSON_Duplicate(config_event, 1); - - // 9. Clean up - cJSON_Delete(config_event); - // 10. Print admin private key for user to save printf("\n"); printf("=================================================================\n"); diff --git a/src/config.h b/src/config.h index 39af68a..bc7504b 100644 --- a/src/config.h +++ b/src/config.h @@ -96,7 +96,7 @@ typedef struct { // Command line options structure for first-time startup typedef struct { int port_override; // -1 = not set, >0 = port value - char admin_privkey_override[65]; // Empty string = not set, 64-char hex = override + char admin_pubkey_override[65]; // Empty string = not set, 64-char hex = override char relay_privkey_override[65]; // Empty string = not set, 64-char hex = override int strict_port; // 0 = allow port increment, 1 = fail if exact port unavailable } cli_options_t; diff --git a/src/main.c b/src/main.c index d06b8b1..eb57682 100644 --- a/src/main.c +++ b/src/main.c @@ -1204,9 +1204,9 @@ void print_usage(const char* program_name) { printf(" -h, --help Show this help message\n"); printf(" -v, --version Show version information\n"); printf(" -p, --port PORT Override relay port (first-time startup only)\n"); - printf(" -a, --admin-privkey HEX Override admin private key (64-char hex)\n"); - printf(" -r, --relay-privkey HEX Override relay private key (64-char hex)\n"); printf(" --strict-port Fail if exact port is unavailable (no port increment)\n"); + printf(" -a, --admin-pubkey HEX Override admin public key (64-char hex)\n"); + printf(" -r, --relay-privkey HEX Override relay private key (64-char hex)\n"); printf("\n"); printf("Configuration:\n"); printf(" This relay uses event-based configuration stored in the database.\n"); @@ -1221,12 +1221,12 @@ void print_usage(const char* program_name) { printf("\n"); printf("Examples:\n"); printf(" %s # Start relay (auto-configure on first run)\n", program_name); - printf(" %s -p 8080 # First-time setup with port 8080\n", program_name); - printf(" %s --port 9000 # First-time setup with port 9000\n", program_name); - printf(" %s --strict-port # Fail if default port 8888 is unavailable\n", program_name); + printf(" %s -p 8080 # First-time setup with port 8080\n", program_name); + printf(" %s --port 9000 # First-time setup with port 9000\n", program_name); + printf(" %s --strict-port # Fail if default port 8888 is unavailable\n", program_name); printf(" %s -p 8080 --strict-port # Fail if port 8080 is unavailable\n", program_name); - printf(" %s --help # Show this help\n", program_name); - printf(" %s --version # Show version info\n", program_name); + printf(" %s --help # Show this help\n", program_name); + printf(" %s --version # Show version info\n", program_name); printf("\n"); } @@ -1242,7 +1242,7 @@ int main(int argc, char* argv[]) { // Initialize CLI options structure cli_options_t cli_options = { .port_override = -1, // -1 = not set - .admin_privkey_override = {0}, // Empty string = not set + .admin_pubkey_override = {0}, // Empty string = not set .relay_privkey_override = {0}, // Empty string = not set .strict_port = 0 // 0 = allow port increment (default) }; @@ -1279,36 +1279,36 @@ int main(int argc, char* argv[]) { char port_msg[128]; snprintf(port_msg, sizeof(port_msg), "Port override specified: %d", cli_options.port_override); log_info(port_msg); - } else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--admin-privkey") == 0) { - // Admin private key override option + } else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--admin-pubkey") == 0) { + // Admin public key override option if (i + 1 >= argc) { - log_error("Admin privkey option requires a value. Use --help for usage information."); + log_error("Admin pubkey option requires a value. Use --help for usage information."); print_usage(argv[0]); return 1; } - - // Validate private key format (must be 64 hex characters) + + // Validate public key format (must be 64 hex characters) if (strlen(argv[i + 1]) != 64) { - log_error("Invalid admin private key length. Must be exactly 64 hex characters."); + log_error("Invalid admin public key length. Must be exactly 64 hex characters."); print_usage(argv[0]); return 1; } - + // Validate hex format for (int j = 0; j < 64; j++) { char c = argv[i + 1][j]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { - log_error("Invalid admin private key format. Must contain only hex characters (0-9, a-f, A-F)."); + log_error("Invalid admin public key format. Must contain only hex characters (0-9, a-f, A-F)."); print_usage(argv[0]); return 1; } } - - strncpy(cli_options.admin_privkey_override, argv[i + 1], sizeof(cli_options.admin_privkey_override) - 1); - cli_options.admin_privkey_override[sizeof(cli_options.admin_privkey_override) - 1] = '\0'; + + strncpy(cli_options.admin_pubkey_override, argv[i + 1], sizeof(cli_options.admin_pubkey_override) - 1); + cli_options.admin_pubkey_override[sizeof(cli_options.admin_pubkey_override) - 1] = '\0'; i++; // Skip the key argument - - log_info("Admin private key override specified"); + + log_info("Admin public key override specified"); } else if (strcmp(argv[i], "-r") == 0 || strcmp(argv[i], "--relay-privkey") == 0) { // Relay private key override option if (i + 1 >= argc) { @@ -1389,7 +1389,7 @@ int main(int argc, char* argv[]) { nostr_cleanup(); return 1; } - + // Now that database is available, store the relay private key securely const char* relay_privkey = get_temp_relay_private_key(); if (relay_privkey) { @@ -1406,17 +1406,36 @@ int main(int argc, char* argv[]) { nostr_cleanup(); return 1; } - - // Systematically add pubkeys to config table - if (add_pubkeys_to_config_table() != 0) { - log_warning("Failed to add pubkeys to config table systematically"); + + // Handle configuration setup after database is initialized + if (cli_options.admin_pubkey_override && strlen(cli_options.admin_pubkey_override) == 64) { + // Admin pubkey provided - populate config table directly + log_info("Populating config table for admin pubkey override after database initialization"); + + // Populate default config values in table + if (populate_default_config_values() != 0) { + log_error("Failed to populate default config values"); + cleanup_configuration_system(); + nostr_cleanup(); + close_database(); + return 1; + } + + // Add pubkeys to config table + if (add_pubkeys_to_config_table() != 0) { + log_error("Failed to add pubkeys to config table"); + cleanup_configuration_system(); + nostr_cleanup(); + close_database(); + return 1; + } + + log_success("Configuration populated directly in config table after database initialization"); } else { - log_success("Pubkeys added to config table systematically"); - } - - // Retry storing the configuration event now that database is initialized - if (retry_store_initial_config_event() != 0) { - log_warning("Failed to store initial configuration event after database init"); + // Admin private key available - retry storing initial config event + if (retry_store_initial_config_event() != 0) { + log_warning("Failed to store initial config event - will retry later"); + } } // Now store the pubkeys in config table since database is available