Files
sovereign_browser/src/main.c
T

977 lines
36 KiB
C

/*
* sovereign_browser — WebKitGTK + C99
*
* Multi-tab browser: a GTK window with a GtkNotebook tab strip. Each tab
* has its own toolbar (hamburger menu + URL entry) and WebKitWebView. All
* webviews share a single WebKitWebContext so the sovereign:// Nostr bridge,
* security settings, and TLS policy apply to every tab.
*
* On startup, a Nostr login dialog is shown. The user signs in with a local
* key (nsec), seed phrase, read-only npub, NIP-46 remote signer, or n_signer
* hardware. The resulting nostr_signer_t is held globally and backs the
* window.nostr injection in every tab.
*
* Features:
* - Tab creation (Ctrl+T, + button), closing (Ctrl+W, close button,
* middle-click)
* - Tab switching (Ctrl+Tab, Ctrl+Shift+Tab, Ctrl+PageUp/Down)
* - Right-click tab context menu (New, Close, Close Others, Close to
* Right, Duplicate, Reload)
* - Tab drag reordering
* - Session save/restore (configurable in Settings)
* - Settings page (sovereign://settings internal webpage: tabs, agent
* server, security — persisted to disk)
*
* Build: make. Run: ./sovereign_browser [url]
*/
#include <gtk/gtk.h>
#include <webkit2/webkit2.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include "version.h"
#include "key_store.h"
#include "login_dialog.h"
#include "nostr_bridge.h"
#include "nostr_scheme.h"
#include "tor_scheme.h"
#include "nostr_inject.h"
#include "history.h"
#include "settings.h"
#include "shortcuts.h"
#include "settings_sync.h"
#include "tab_manager.h"
#include "session.h"
#include "agent_server.h"
#include "agent_login.h"
#include "cli.h"
#include "db.h"
#include "profile.h"
#include "relay_fetch.h"
#include "bookmarks.h"
#include "agent_conversations.h"
#include "agent_skills.h"
#include "net_services.h"
#include "nostr_core/nostr_core.h"
/* ---- Global state --------------------------------------------------- *
* The signer is created at login and held for the lifetime of the session.
* It backs the window.nostr injection in every tab via the sovereign://
* URI scheme bridge.
*/
typedef struct {
nostr_signer_t *signer; /* NULL for read-only mode */
char pubkey_hex[65];
key_store_method_t method;
gboolean readonly; /* TRUE if no signing available */
} app_state_t;
static app_state_t g_state = {0};
/* Forward declaration — defined before main(). */
static int switch_to_user_db(const char *pubkey_hex);
static GtkWindow *g_window = NULL;
static gboolean g_logged_in = FALSE;
static gboolean g_is_fullscreen = FALSE; /* track fullscreen state (GTK3 has no getter */
/* ---- App state accessors (used by agent_login.c) ─────────────────── */
void app_set_signer(nostr_signer_t *signer, const char *pubkey_hex,
key_store_method_t method, gboolean readonly) {
g_state.signer = signer;
g_state.method = method;
g_state.readonly = readonly;
if (pubkey_hex) {
strncpy(g_state.pubkey_hex, pubkey_hex, 64);
g_state.pubkey_hex[64] = '\0';
}
g_logged_in = TRUE;
/* Update modules that hold a signer reference. */
settings_sync_set_signer(signer,
g_state.pubkey_hex[0] ? g_state.pubkey_hex : NULL);
agent_conversations_set_signer(signer,
g_state.pubkey_hex[0] ? g_state.pubkey_hex : NULL);
/* If this is the first login (we're still on global.db), switch to
* the per-user profile database. If we're already on a per-user db
* (e.g. switching identity at runtime), switch_to_user_db() will
* close it and open the new user's db. */
if (g_state.pubkey_hex[0] != '\0') {
switch_to_user_db(g_state.pubkey_hex);
}
}
void app_clear_signer(void) {
if (g_state.signer) {
nostr_signer_free(g_state.signer);
g_state.signer = NULL;
}
g_state.pubkey_hex[0] = '\0';
g_state.method = KEY_STORE_METHOD_NONE;
g_state.readonly = FALSE;
g_logged_in = FALSE;
settings_sync_set_signer(NULL, NULL);
agent_conversations_set_signer(NULL, NULL);
}
nostr_signer_t *app_get_signer(void) { return g_state.signer; }
const char *app_get_pubkey_hex(void) { return g_state.pubkey_hex; }
key_store_method_t app_get_method(void) { return g_state.method; }
gboolean app_get_readonly(void) { return g_state.readonly; }
/* ---- Menu proxy functions ------------------------------------------- *
* These wrap the app_state_t-aware callbacks so they match GTK signal
* handler signatures and can be called from tab_manager.c's hamburger
* menu builder.
*/
void app_menu_switch_identity_proxy(GtkMenuItem *item, gpointer data) {
(void)item;
GtkWindow *window = GTK_WINDOW(data);
if (window == NULL) return;
login_result_t result;
if (login_dialog_run(window, &result) == 0) {
if (g_state.signer) {
nostr_signer_free(g_state.signer);
}
g_state.signer = result.signer;
g_state.method = result.method;
strncpy(g_state.pubkey_hex, result.pubkey_hex, 64);
g_state.pubkey_hex[64] = '\0';
g_state.readonly = (result.method == KEY_STORE_METHOD_READONLY);
nostr_bridge_set_signer(g_state.signer, g_state.pubkey_hex,
g_state.readonly);
/* Switch to the new user's per-user profile database. This
* closes the current browser.db and opens the new user's
* browser.db, then loads their per-user settings. */
if (g_state.pubkey_hex[0] != '\0') {
switch_to_user_db(g_state.pubkey_hex);
}
g_print("[identity] switched: method=%d pubkey=%s\n",
g_state.method, g_state.pubkey_hex);
}
}
void app_menu_lock_session_proxy(GtkMenuItem *item, gpointer data) {
(void)item;
(void)data;
if (g_state.signer) {
nostr_signer_free(g_state.signer);
g_state.signer = NULL;
}
g_state.readonly = TRUE;
nostr_bridge_set_signer(NULL, g_state.pubkey_hex, TRUE);
g_print("[identity] session locked (signer cleared, identity preserved)\n");
}
void app_menu_logout_proxy(GtkMenuItem *item, gpointer data) {
(void)item;
(void)data;
if (g_state.signer) {
nostr_signer_free(g_state.signer);
g_state.signer = NULL;
}
g_state.pubkey_hex[0] = '\0';
g_state.method = KEY_STORE_METHOD_NONE;
g_state.readonly = FALSE;
g_print("[identity] logged out\n");
}
void app_menu_security_strip_proxy(GtkCheckMenuItem *item, gpointer data) {
(void)data;
gboolean active = gtk_check_menu_item_get_active(item);
g_print("[menu] security strip: %s\n", active ? "ON" : "OFF");
}
void app_menu_network_service_proxy(GtkCheckMenuItem *item, gpointer data) {
if (g_object_get_data(G_OBJECT(item), "network-toggle-sync") != NULL) return;
net_service_type_t type = (net_service_type_t)GPOINTER_TO_INT(data);
if (type != NET_SERVICE_TOR && type != NET_SERVICE_FIPS) {
g_printerr("[menu.network] Invalid network service type %d\n", (int)type);
return;
}
const char *name = type == NET_SERVICE_TOR ? "Tor" : "FIPS";
gboolean enabled = gtk_check_menu_item_get_active(item);
browser_settings_t *settings = settings_get_mutable();
gboolean *setting = type == NET_SERVICE_TOR
? &settings->tor_enabled : &settings->fips_enabled;
*setting = enabled;
settings_save_user();
int rc = enabled ? net_service_enable(type) : net_service_disable(type);
if (enabled && rc != 0) {
const net_service_t *status = net_service_get_status(type);
g_printerr("[menu.network] Failed to enable %s service: %s; reverting preference\n",
name, status && status->error_msg
? status->error_msg : "synchronous startup failure");
*setting = FALSE;
settings_save_user();
g_object_set_data(G_OBJECT(item), "network-toggle-sync",
GINT_TO_POINTER(1));
gtk_check_menu_item_set_active(item, FALSE);
g_object_set_data(G_OBJECT(item), "network-toggle-sync", NULL);
return;
}
if (rc != 0) {
g_printerr("[menu.network] Failed to disable %s service (preference remains disabled)\n",
name);
} else {
g_print("[menu.network] %s service %s; per-user preference saved\n",
name, enabled ? "enabled" : "disabled");
}
}
void app_menu_nostr_sign_proxy(GtkMenuItem *item, gpointer data) {
(void)item;
(void)data;
if (g_state.signer) {
g_print("[menu] Nostr signing: signer active, pubkey=%s\n",
g_state.pubkey_hex);
} else if (g_state.readonly) {
g_print("[menu] Nostr signing: read-only mode (no signer)\n");
} else {
g_print("[menu] Nostr signing: no signer loaded\n");
}
}
void app_menu_about_proxy(GtkMenuItem *item, gpointer data) {
(void)item;
GtkWidget *window = GTK_WIDGET(data);
if (window == NULL) return;
GtkWidget *dialog = gtk_message_dialog_new(
GTK_WINDOW(window),
GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_MESSAGE_INFO,
GTK_BUTTONS_OK,
"sovereign browser %s\n"
"WebKitGTK + C99 — sovereign identity, not permissioned domains.",
SB_VERSION);
g_signal_connect(dialog, "response", G_CALLBACK(gtk_widget_destroy), NULL);
gtk_widget_show_all(dialog);
}
/* ---- Settings page (internal webpage) ------------------------------- *
* The hamburger-menu "Settings…" item navigates the active tab to the
* sovereign://settings internal page, which renders all preferences
* (tabs, agent server, security) and persists changes via the
* sovereign:// URI scheme bridge in nostr_bridge.c.
*/
void on_menu_settings(GtkMenuItem *item, gpointer data) {
(void)item;
(void)data;
tab_info_t *tab = tab_manager_get_active();
if (tab && tab->webview) {
webkit_web_view_load_uri(tab->webview, "sovereign://settings");
} else {
/* No active tab — open a new one pointed at the settings page. */
tab_manager_new_tab("sovereign://settings");
}
}
void on_menu_profile(GtkMenuItem *item, gpointer data) {
(void)item;
(void)data;
tab_info_t *tab = tab_manager_get_active();
if (tab && tab->webview) {
webkit_web_view_load_uri(tab->webview, "sovereign://profile");
} else {
tab_manager_new_tab("sovereign://profile");
}
}
/* The hamburger-menu "Agent Setup…" item navigates the active tab to
* the sovereign://agents internal page, which renders the agent provider
* configuration UI and a link to open the agent chat. */
void on_menu_agent(GtkMenuItem *item, gpointer data) {
(void)item;
(void)data;
tab_info_t *tab = tab_manager_get_active();
if (tab && tab->webview) {
webkit_web_view_load_uri(tab->webview, "sovereign://agents");
} else {
/* No active tab — open a new one pointed at the agent page. */
tab_manager_new_tab("sovereign://agents");
}
}
/* ---- Keyboard shortcuts --------------------------------------------- *
* All browser-level shortcuts are configurable via the shortcuts module.
* The on_key_press handler looks up the action for the pressed key
* combination and dispatches it. Bindings are set on the
* sovereign://settings page (key-capture UI) and persisted to SQLite.
*/
/* Made non-static so tab_manager.c can connect it to each webview.
*
* This handler is connected to two places:
* 1. Each webview's "key-press-event" (in tab_manager.c tab_create())
* 2. The main window's "key-press-event" (in main.c setup)
*
* For the webview connection, we bypass shortcut interception when the
* active page is a sovereign:// internal page — this lets the page's JS
* capture arbitrary key combos (e.g. the settings page's shortcut capture
* UI needs to see Ctrl+key events that would otherwise be intercepted).
*
* For the window connection, we always process shortcuts so that
* browser-level shortcuts (Ctrl+T, Ctrl+N, etc.) work even on
* sovereign:// pages. The window handler is a fallback that fires after
* the webview handler (event propagation: child → parent → window). */
gboolean on_key_press(GtkWidget *widget, GdkEventKey *event,
gpointer data) {
(void)data;
/* Check if this handler is connected to a webview (not the window).
* WEBKIT_IS_WEB_VIEW is true for webview connections, false for the
* window connection. */
gboolean from_webview = WEBKIT_IS_WEB_VIEW(widget);
/* When the active tab is showing a sovereign:// internal page (e.g.
* the settings page's keyboard-shortcut capture), bypass shortcut
* interception on the webview connection only — let the event pass
* through to the web page's JS so it can capture arbitrary key combos.
* The window-level connection still processes shortcuts so Ctrl+T,
* Ctrl+N, etc. work on internal pages. */
if (from_webview) {
tab_info_t *active = tab_manager_get_active();
if (active && active->current_url[0] != '\0' &&
strncmp(active->current_url, "sovereign://", 12) == 0) {
return FALSE;
}
}
int action = shortcuts_lookup(event);
if (action < 0) return FALSE;
switch ((shortcut_action_t)action) {
case SHORTCUT_NEW_TAB:
tab_manager_new_tab(NULL);
return TRUE;
case SHORTCUT_NEW_WINDOW:
tab_manager_new_window_blank();
return TRUE;
case SHORTCUT_CLOSE_TAB:
tab_manager_close_active();
return TRUE;
case SHORTCUT_FOCUS_URL: {
tab_info_t *tab = tab_manager_get_active();
if (tab && tab->url_entry) {
gtk_widget_grab_focus(tab->url_entry);
gtk_editable_select_region(GTK_EDITABLE(tab->url_entry), 0, -1);
}
return TRUE;
}
case SHORTCUT_NEXT_TAB:
tab_manager_next();
return TRUE;
case SHORTCUT_PREV_TAB:
tab_manager_prev();
return TRUE;
case SHORTCUT_NEXT_TAB_PAGEDOWN:
tab_manager_next();
return TRUE;
case SHORTCUT_PREV_TAB_PAGEUP:
tab_manager_prev();
return TRUE;
case SHORTCUT_RELOAD: {
tab_info_t *tab = tab_manager_get_active();
if (tab && tab->webview)
webkit_web_view_reload(tab->webview);
return TRUE;
}
case SHORTCUT_FORCE_RELOAD: {
tab_info_t *tab = tab_manager_get_active();
if (tab && tab->webview)
webkit_web_view_reload_bypass_cache(tab->webview);
return TRUE;
}
case SHORTCUT_GO_BACK: {
tab_info_t *tab = tab_manager_get_active();
if (tab && tab->webview && webkit_web_view_can_go_back(tab->webview))
webkit_web_view_go_back(tab->webview);
return TRUE;
}
case SHORTCUT_GO_FORWARD: {
tab_info_t *tab = tab_manager_get_active();
if (tab && tab->webview && webkit_web_view_can_go_forward(tab->webview))
webkit_web_view_go_forward(tab->webview);
return TRUE;
}
case SHORTCUT_FIND:
/* Phase 2: wire WebKitFindController + find bar.
* For now, log so the user knows the binding fired. */
g_print("[shortcut] Find in page (not yet implemented)\n");
return TRUE;
case SHORTCUT_OPEN_SETTINGS:
on_menu_settings(NULL, NULL);
return TRUE;
case SHORTCUT_NEW_IDENTITY:
app_menu_switch_identity_proxy(NULL, g_window);
return TRUE;
case SHORTCUT_TOGGLE_FULLSCREEN: {
if (g_is_fullscreen) {
gtk_window_unfullscreen(g_window);
g_is_fullscreen = FALSE;
} else {
gtk_window_fullscreen(g_window);
g_is_fullscreen = TRUE;
}
return TRUE;
}
case SHORTCUT_TOGGLE_INSPECTOR:
tab_manager_toggle_inspector();
return TRUE;
case SHORTCUT_TOGGLE_SIDEBAR:
tab_manager_toggle_sidebar();
return TRUE;
case SHORTCUT_ZOOM_IN:
tab_manager_zoom_in();
return TRUE;
case SHORTCUT_ZOOM_OUT:
tab_manager_zoom_out();
return TRUE;
case SHORTCUT_ZOOM_RESET:
tab_manager_zoom_reset();
return TRUE;
default:
return FALSE;
}
}
/* ---- Agent login callback ------------------------------------------ *
* Called by agent_server when an agent successfully logs in via the
* 'login' tool. The agent can log in at any time — while the GTK login
* dialog is showing, or after the browser is already running. We just
* set the flag; the do_login function checks it after the dialog
* returns and skips the dialog's result if the agent already logged in.
*/
static void agent_login_callback(void) {
g_logged_in = TRUE;
g_print("[login] Agent login detected.\n");
}
/* ---- Window destroy ------------------------------------------------- */
static void on_window_destroy(GtkWidget *widget, gpointer data) {
(void)widget;
(void)data;
const browser_settings_t *s = settings_get();
if (s->restore_session) {
/* Save the session for next launch. */
session_save();
} else {
/* Privacy mode: clear session and history on shutdown. */
db_session_clear();
history_clear();
g_print("[shutdown] Cleared session and history (restore_session=off)\n");
}
/* Stop browser-managed Tor/FIPS before tearing down GTK/WebKit. */
net_services_shutdown();
/* Stop the agent server. */
agent_server_stop();
if (g_state.signer) {
nostr_signer_free(g_state.signer);
g_state.signer = NULL;
}
nostr_cleanup();
bookmarks_cleanup();
db_close();
gtk_main_quit();
}
/* ---- Login flow (GTK dialog) ─────────────────────────────────────── *
* Shows the GTK login dialog. The agent server is already running at
* this point, so an agent can call 'login' while the dialog is showing
* (gtk_dialog_run runs a nested main loop that processes WebSocket
* events). After the dialog returns, we check if the agent already
* logged in — if so, we discard the dialog's result.
*/
static int do_login(GtkWindow *parent) {
/* nostr_init() is already called before this function. */
login_result_t result;
if (login_dialog_run(parent, &result) != 0) {
/* Dialog was cancelled. But if the agent logged in while the
* dialog was showing, proceed with the agent's login. */
if (g_logged_in) {
return 0;
}
return -1;
}
/* If the agent already logged in while the dialog was showing,
* discard the dialog's result and use the agent's login. */
if (g_logged_in) {
g_print("[login] Agent login took priority over dialog.\n");
if (result.signer) {
nostr_signer_free(result.signer);
}
return 0;
}
g_state.signer = result.signer;
g_state.method = result.method;
strncpy(g_state.pubkey_hex, result.pubkey_hex, 64);
g_state.pubkey_hex[64] = '\0';
g_state.readonly = (result.method == KEY_STORE_METHOD_READONLY ||
result.method == KEY_STORE_METHOD_NONE);
g_logged_in = TRUE;
if (result.method == KEY_STORE_METHOD_NONE) {
g_print("[login] No-login mode (browsing without Nostr identity)\n");
} else {
g_print("[login] New identity: method=%d pubkey=%s\n",
g_state.method, g_state.pubkey_hex);
}
return 0;
}
/* ---- Per-user database switch --------------------------------------- *
* After login, switch from the global database to the per-user profile
* database. Creates the profile directory if needed, closes global.db,
* opens the per-user browser.db, and loads per-user settings.
*
* This function is called from:
* - app_set_signer() — when an agent logs in via MCP or CLI
* - app_menu_switch_identity_proxy() — runtime identity switch via menu
* - main() — after the GTK login dialog returns
*
* It is idempotent: if already on the correct per-user db, it's a no-op.
*/
/* Track the currently-open per-user db path so we can skip re-opening
* the same database (e.g. when app_set_signer() is called during the
* login dialog and then main() calls switch_to_user_db() again). */
static char g_current_profile_db[512] = "";
static int switch_to_user_db(const char *pubkey_hex) {
if (pubkey_hex == NULL || pubkey_hex[0] == '\0') {
g_printerr("[profile] No pubkey, staying on global.db\n");
return -1;
}
/* Create the profile directory. */
if (profile_ensure_dir(pubkey_hex) != 0) {
g_printerr("[profile] Failed to create profile dir for %s\n",
pubkey_hex);
return -1;
}
/* Open the per-user browser.db (closes global.db first). */
char db_path[512];
profile_get_db_path(pubkey_hex, db_path, sizeof(db_path));
if (db_path[0] == '\0') {
g_printerr("[profile] Failed to get db path for %s\n", pubkey_hex);
return -1;
}
/* Skip if already on this database (idempotent). */
if (g_current_profile_db[0] != '\0' &&
strcmp(g_current_profile_db, db_path) == 0) {
g_print("[profile] Already on per-user db: %s\n", db_path);
return 0;
}
if (db_init_with_path(db_path) != 0) {
g_printerr("[profile] Failed to open per-user db: %s\n", db_path);
return -1;
}
/* Record the current profile db path (for idempotency check). */
snprintf(g_current_profile_db, sizeof(g_current_profile_db),
"%s", db_path);
/* Load per-user settings from the per-user browser.db. This only
* reads per-user keys; global settings already in memory are
* preserved. */
settings_load_user();
/* Save the last-used pubkey to the global identity.json so the
* login dialog can default to this profile next time. */
profile_save_last_pubkey(pubkey_hex);
/* Save public identity info (method, pubkey) to the per-profile
* identity.json. Private keys are never stored. */
key_store_save_profile_identity(pubkey_hex, g_state.method);
g_print("[profile] Switched to per-user db: %s\n", db_path);
return 0;
}
/* ---- Main ----------------------------------------------------------- */
int main(int argc, char **argv) {
(void)signal(SIGPIPE, SIG_IGN);
/* Parse CLI flags BEFORE gtk_init() so GTK doesn't abort on our
* flags. cli_parse() strips recognized flags from argv and collects
* positional URLs. */
cli_args_t cli;
if (cli_parse(&argc, &argv, &cli) != 0) {
cli_args_free(&cli);
return EXIT_FAILURE;
}
if (cli.want_help) {
cli_print_usage(stdout);
cli_args_free(&cli);
return EXIT_SUCCESS;
}
if (cli.want_version) {
printf("sovereign_browser %s\n", SB_VERSION);
cli_args_free(&cli);
return EXIT_SUCCESS;
}
gtk_init(&argc, &argv);
/* NOTE: key_store_delete_legacy_file() was previously called here
* to delete identity.json from old versions that persisted private
* keys. We no longer call it because identity.json is now used
* legitimately to store the last-used pubkey_hex (no private keys).
* This is a new project with no legacy files to migrate. */
/* Initialize the global database first — global settings and
* shortcuts are stored there. The per-user browser.db is opened
* after login, once we know the user's pubkey. */
db_init_global();
/* Set defaults, then load global settings from global.db. Per-user
* settings are loaded after login from the per-user browser.db. */
{
browser_settings_t *s = settings_get_mutable();
/* settings_load_global() does NOT reset defaults, so we need to
* set them first. We call settings_load() which sets defaults
* and then loads global settings from the currently-open db
* (global.db). The per-user keys won't be found in global.db,
* so their defaults are kept. */
(void)s;
settings_load();
}
/* Load keyboard shortcut bindings from the database (global.db).
* Must come after settings_load() because shortcuts_lookup() checks
* the master ctrl_tab_switch toggle. Shortcuts are global — they
* are the same across all profiles. */
shortcuts_load();
/* History is queried from the SQLite database on demand — no
* separate load step needed. */
/* Apply CLI overrides to the in-memory settings singleton. These
* do not write to disk — they are one-shot overrides for this run. */
{
browser_settings_t *s = settings_get_mutable();
if (cli.new_tab_url) {
snprintf(s->new_tab_url, sizeof(s->new_tab_url), "%s",
cli.new_tab_url);
}
if (cli.max_tabs > 0) {
s->max_tabs = cli.max_tabs;
}
if (cli.session_restore == CLI_TRISTATE_TRUE) {
s->restore_session = TRUE;
} else if (cli.session_restore == CLI_TRISTATE_FALSE) {
s->restore_session = FALSE;
}
if (cli.agent_port > 0) {
s->agent_server_port = cli.agent_port;
}
if (cli.agent_enabled == CLI_TRISTATE_TRUE) {
s->agent_server_enabled = TRUE;
} else if (cli.agent_enabled == CLI_TRISTATE_FALSE) {
s->agent_server_enabled = FALSE;
}
if (cli.login_timeout_ms > 0) {
s->agent_login_timeout_ms = cli.login_timeout_ms;
}
/* --agent-origin: append to allowed origins (comma-separated). */
if (cli.agent_origin_count > 0) {
GString *origins = g_string_new(s->agent_allowed_origins);
for (int i = 0; i < cli.agent_origin_count; i++) {
if (origins->len > 0) g_string_append_c(origins, ',');
g_string_append(origins, cli.agent_origins[i]);
}
snprintf(s->agent_allowed_origins, sizeof(s->agent_allowed_origins),
"%s", origins->str);
g_string_free(origins, TRUE);
}
}
/* Top-level window. */
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(window), "sovereign browser " SB_VERSION);
gtk_window_set_default_size(GTK_WINDOW(window), 1024, 768);
g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), NULL);
g_signal_connect(window, "key-press-event", G_CALLBACK(on_key_press), NULL);
g_window = GTK_WINDOW(window);
/* Start the agent server before login. The server runs on the
* configured port (default 17777) and is available throughout the
* entire browser lifecycle — an agent can log in at any time, even
* while the GTK login dialog is showing or after the browser is
* already running. If disabled in settings, skip it. */
const browser_settings_t *s = settings_get();
if (s->agent_server_enabled) {
if (agent_server_start(s->agent_server_port) == 0) {
g_print("[agent] Server started on port %d\n",
agent_server_get_port());
} else {
g_printerr("[agent] Failed to start server on port %d\n",
s->agent_server_port);
}
}
/* Initialize nostr_core_lib (needed for both agent and GTK login). */
if (nostr_init() != NOSTR_SUCCESS) {
g_printerr("[login] Failed to initialize nostr_core_lib\n");
agent_server_stop();
cli_args_free(&cli);
return EXIT_SUCCESS;
}
/* Set up the login callback so if an agent calls 'login' while the
* GTK dialog is showing, we can close the dialog and proceed. */
agent_server_set_login_callback(agent_login_callback);
/* If --no-login was given, skip login entirely. The browser works
* as a normal browser without a Nostr identity. window.nostr won't
* be available for sign requests, but all browsing works. */
if (cli.no_login) {
g_logged_in = TRUE;
g_state.readonly = TRUE;
g_print("[login] No-login mode (browsing without Nostr identity)\n");
}
/* If --login-method was given on the command line, perform CLI login
* now and skip the GTK dialog entirely. This reuses agent_login(),
* the same code path as the MCP 'login' tool. */
if (cli.login_method) {
if (cli_login(&cli) != 0) {
g_printerr("[login] CLI login failed, exiting.\n");
agent_server_stop();
nostr_cleanup();
cli_args_free(&cli);
return EXIT_FAILURE;
}
g_logged_in = TRUE;
}
/* Show the GTK login dialog immediately — no blocking wait.
* The dialog runs a nested GTK main loop (gtk_dialog_run) which
* processes WebSocket events, so the agent server is live during
* the dialog. If an agent calls 'login' while the dialog is open,
* the callback fires and we close the dialog.
*
* If the agent logs in before the dialog appears (unlikely but
* possible), skip the dialog entirely. */
if (!g_logged_in) {
if (do_login(g_window) != 0) {
g_print("[login] Cancelled, exiting.\n");
agent_server_stop();
nostr_cleanup();
cli_args_free(&cli);
return EXIT_SUCCESS;
}
}
/* ── Switch to per-user profile database ────────────────────── *
* Now that we know the user's pubkey, close global.db and open the
* per-user browser.db at ~/.sovereign_browser/profiles/<pubkey>/.
* This must happen before relay_fetch, session_restore, bookmarks,
* etc. — all of which read/write the per-user database.
*
* If there's no pubkey (--no-login mode), we stay on global.db.
* Browsing still works; history/session just go to global.db in
* that case (acceptable for the no-identity mode). */
if (g_state.pubkey_hex[0] != '\0') {
if (switch_to_user_db(g_state.pubkey_hex) != 0) {
g_printerr("[profile] Failed to switch to per-user db — "
"continuing with global.db\n");
}
}
/* If the user logged in with a Nostr identity (not --no-login), start
* a background thread to fetch their kind 0/3/10002 events from the
* bootstrap relays. The results are cached in the per-user SQLite
* database. The thread frees the pubkey copy when done. */
if (g_state.pubkey_hex[0] != '\0') {
char *pubkey_copy = g_strdup(g_state.pubkey_hex);
g_thread_new("relay-fetch", relay_fetch_thread, pubkey_copy);
g_print("[relay] Bootstrap fetch thread started for %s\n",
g_state.pubkey_hex);
}
/* Initialize the bookmarks module. Loads cached kind 30003 events
* from SQLite and decrypts them. In no-login/read-only mode, the
* signer is NULL so bookmarks are read-only. */
bookmarks_init(g_state.signer,
g_state.pubkey_hex[0] ? g_state.pubkey_hex : NULL);
/* Initialize the NIP-78 settings sync module. In no-login/read-only
* mode, the signer is NULL so settings are local-only (not synced). */
settings_sync_init(g_state.signer,
g_state.pubkey_hex[0] ? g_state.pubkey_hex : NULL);
/* Initialize the conversation persistence module (kind 30078
* conversations). In no-login/read-only mode, the signer is NULL
* so conversations are not encrypted/synced. */
agent_conversations_init(g_state.signer,
g_state.pubkey_hex[0] ? g_state.pubkey_hex : NULL);
/* Initialize the skills module (kind 31123 public skill events).
* In no-login/read-only mode, the signer is NULL so skills cannot
* be published/deleted (but can still be fetched and selected). */
agent_skills_init(g_state.signer,
g_state.pubkey_hex[0] ? g_state.pubkey_hex : NULL);
/* Set the user's avatar on the tab bar from their kind 0 profile.
* If the profile hasn't been fetched yet (relay fetch is still
* running), this shows the default icon. The avatar will be updated
* when the relay fetch completes and the kind 0 event is stored. */
tab_manager_set_avatar(g_state.pubkey_hex[0] ? g_state.pubkey_hex : NULL);
/* Use the default WebKitWebContext — it comes with proper networking
* (cookies, cache, soup session) that a freshly created context lacks.
* All tabs will create webviews from this shared context. */
WebKitWebContext *web_ctx = webkit_web_context_get_default();
/* ── Security strip: disable web security restrictions ─────── */
WebKitSecurityManager *sec_mgr =
webkit_web_context_get_security_manager(web_ctx);
/* Register sovereign:// as secure only. Do NOT register it as "local"
* (WebKit blocks fetch from https to local origins) and do NOT register
* it as "cors_enabled" (that makes WebKit enforce CORS response headers,
* which we can't set with the basic finish API). Without these, WebKit
* treats sovereign:// as a simple secure scheme with no CORS checks. */
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "sovereign");
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "tor");
webkit_security_manager_register_uri_scheme_as_secure(sec_mgr, "file");
webkit_security_manager_register_uri_scheme_as_local(sec_mgr, "file");
/* Accept any TLS certificate (FIPS uses Noise IK, not TLS CAs). */
WebKitWebsiteDataManager *data_mgr =
webkit_web_context_get_website_data_manager(web_ctx);
webkit_website_data_manager_set_tls_errors_policy(
data_mgr, WEBKIT_TLS_ERRORS_POLICY_IGNORE);
/* Enable the favicon database so WebKitGTK automatically fetches and
* caches favicons for visited pages. Without this, the
* "notify::favicon" signal never fires and webkit_web_view_get_favicon()
* always returns NULL. Passing NULL for the directory uses WebKit's
* default cache location. */
webkit_web_context_set_favicon_database_directory(web_ctx, NULL);
g_print("[main] Favicon database enabled\n");
/* Initialize browser-managed/attached network services after login and
* after the shared WebKit context/data manager have been configured. */
net_services_init();
/* Register the sovereign:// URI scheme for the window.nostr bridge. */
nostr_bridge_register(web_ctx, g_state.signer, g_state.pubkey_hex,
g_state.readonly);
/* Register nostr:// entity pages before creating any webviews. */
nostr_scheme_register(web_ctx);
tor_scheme_register(web_ctx);
/* Vertical box: the tab manager's notebook fills the window. */
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_container_add(GTK_CONTAINER(window), vbox);
/* Initialize the tab manager. */
tab_manager_init(GTK_CONTAINER(vbox), web_ctx, g_window);
/* Decide whether to restore the previous session or open CLI URLs.
*
* Precedence:
* 1. --no-session-restore → skip restore, open CLI URLs (or default)
* 2. --url / positional → skip restore, open the given URLs
* 3. --session-restore → force restore
* 4. settings.restore_session (default) → restore if enabled
*
* If restore succeeds, CLI URLs are ignored (the user wanted their
* session back). If restore fails or is skipped, open CLI URLs; if
* none were given, open a single tab with the default new-tab URL. */
gboolean skip_restore = (cli.session_restore == CLI_TRISTATE_FALSE) ||
(cli.url_count > 0);
int restored = skip_restore ? 0 : session_restore();
if (restored == 0) {
if (cli.url_count > 0) {
for (int i = 0; i < cli.url_count; i++) {
tab_manager_new_tab(cli.urls[i]);
}
} else {
const char *url = settings_get()->new_tab_url;
tab_manager_new_tab(url);
}
}
/* Wire the security refs to the first tab's settings (settings are
* per-webview, not per-context). The sovereign://security page uses
* these to read and toggle security features. */
{
tab_info_t *first_tab = tab_manager_get(0);
if (first_tab && first_tab->webview) {
WebKitSettings *settings =
webkit_web_view_get_settings(first_tab->webview);
nostr_bridge_set_security_refs(settings, sec_mgr);
}
}
cli_args_free(&cli);
gtk_widget_show_all(window);
/* Re-hide the sidebar container that show_all revealed. The sidebar
* is per-window (packed in the window-level GtkPaned) and should
* only appear when the user toggles it. */
tab_manager_hide_sidebar_after_show_all();
gtk_main();
return EXIT_SUCCESS;
}