1248 lines
49 KiB
C
1248 lines
49 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 "webkit_data.h"
|
|
#include "web_context.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];
|
|
char privkey_hex[65]; /* in-memory only; for HMAC d-tag derivation */
|
|
key_store_method_t method;
|
|
gboolean readonly; /* TRUE if no signing available */
|
|
} app_state_t;
|
|
|
|
static app_state_t g_state = {0};
|
|
|
|
/* Forward declarations — defined later in this file. */
|
|
static int switch_to_user_db(const char *pubkey_hex);
|
|
static int do_login(GtkWindow *parent);
|
|
static WebKitWebContext *build_context_for_current_user(void);
|
|
static char g_current_profile_db[512];
|
|
static GtkWindow *g_window = NULL;
|
|
static gboolean g_logged_in = FALSE;
|
|
static gboolean g_is_fullscreen = FALSE; /* track fullscreen state (GTK3 has no getter */
|
|
|
|
/* TRUE once main() has finished initial startup (tab_manager_init has
|
|
* run). Used by app_set_signer() to distinguish a first-time login
|
|
* (context built later by main()) from a runtime identity switch
|
|
* (context was torn down by identity_teardown_web_state and must be
|
|
* rebuilt here). */
|
|
static gboolean g_post_startup = FALSE;
|
|
|
|
/* ---- App state accessors (used by agent_login.c) ─────────────────── */
|
|
|
|
void app_set_signer(nostr_signer_t *signer, const char *pubkey_hex,
|
|
const char *privkey_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';
|
|
}
|
|
if (privkey_hex && privkey_hex[0]) {
|
|
strncpy(g_state.privkey_hex, privkey_hex, 64);
|
|
g_state.privkey_hex[64] = '\0';
|
|
} else {
|
|
g_state.privkey_hex[0] = '\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);
|
|
}
|
|
|
|
/* If this is a RUNTIME identity switch (past startup) and the
|
|
* previous context was torn down by identity_teardown_web_state(),
|
|
* rebuild a fresh per-user context for the new identity now. On the
|
|
* first login (before main() calls tab_manager_init), g_post_startup
|
|
* is FALSE and web_context_get() is NULL, so main() builds the
|
|
* context after login — we skip here to avoid a double-build. */
|
|
if (g_post_startup && web_context_get() == NULL) {
|
|
g_print("[identity] Rebuilding web context for new identity (runtime switch)\n");
|
|
if (build_context_for_current_user() != NULL) {
|
|
/* Open a fresh tab for the new user so they don't stare at
|
|
* an empty window after the teardown closed all tabs. */
|
|
tab_manager_new_tab(settings_get()->new_tab_url);
|
|
} else {
|
|
g_printerr("[identity] Failed to rebuild web context\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
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.privkey_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.
|
|
*/
|
|
|
|
/* ── Web state teardown (Phase 0 of plans/webkit-data-isolation.md) ── *
|
|
* Called on logout and identity switch. Closes all tabs, destroys the
|
|
* agent chat sidebar webviews (which hold a long-lived WebKitWebView
|
|
* with the previous user's sovereign://agents/chat session), and wipes
|
|
* all WebKit website data (cookies, cache, localStorage, IndexedDB,
|
|
* service workers, favicons) from the shared WebKitWebsiteDataManager.
|
|
*
|
|
* Without this, the next user inherits the previous user's web session:
|
|
* cookies identify you to web pages, localStorage holds per-site state,
|
|
* and the live tabs keep the previous user's DOM in memory. See
|
|
* plans/webkit-data-isolation.md for the full rationale.
|
|
*
|
|
* This must run on the GTK main thread. It pumps a nested main loop
|
|
* briefly while webkit_website_data_manager_clear() completes.
|
|
*/
|
|
void identity_teardown_web_state(void) {
|
|
/* 1. Close all tabs. This destroys the webviews and drops their
|
|
* in-memory DOM/localStorage. Must happen before the context
|
|
* teardown so no webviews hold dangling references to the old
|
|
* context. Suppress the normal "last tab closed → quit the app"
|
|
* behavior so the browser stays alive for the next user; the
|
|
* caller opens a fresh tab after the switch. */
|
|
tab_manager_set_suppress_quit_on_last_tab(TRUE);
|
|
tab_manager_close_all();
|
|
tab_manager_set_suppress_quit_on_last_tab(FALSE);
|
|
|
|
/* 2. Destroy the agent chat sidebar webviews in every window. The
|
|
* sidebar webview is outside the notebook so tab_manager_close_all
|
|
* does not touch it; it would otherwise keep a reference to the
|
|
* old context (and the previous user's chat session) alive. */
|
|
tab_manager_destroy_sidebar_webviews();
|
|
|
|
/* 3. Tear down the per-user WebKitWebContext. This unrefs the
|
|
* context and its per-user WebKitWebsiteDataManager. With Phase A
|
|
* isolation, each user has their own data manager rooted at
|
|
* profiles/<pubkey>/webkit/, so tearing down the context is what
|
|
* actually isolates the users — the next build creates a fresh
|
|
* data manager for the new user. The Phase 0 webkit_data_clear_all
|
|
* wipe is no longer needed for isolation (the data manager is
|
|
* per-user), but we keep it as a defense-in-depth safety net in
|
|
* case any process-global WebKit state survived the context
|
|
* teardown. */
|
|
WebKitWebContext *ctx = web_context_get();
|
|
if (ctx != NULL) {
|
|
/* Defense-in-depth: clear the data manager before tearing down
|
|
* the context. This catches any process-global caches that
|
|
* outlive the context. */
|
|
int rc = webkit_data_clear_all(ctx, FALSE);
|
|
if (rc != 0) {
|
|
g_printerr("[identity] webkit_data_clear_all returned %d "
|
|
"(continuing with context teardown)\n", rc);
|
|
}
|
|
web_context_teardown();
|
|
}
|
|
}
|
|
|
|
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) {
|
|
/* Tear down the previous user's web state BEFORE installing the
|
|
* new signer, so the new user starts with a clean web session. */
|
|
identity_teardown_web_state();
|
|
|
|
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);
|
|
}
|
|
|
|
/* Build a fresh per-user WebKitWebContext for the new identity
|
|
* (with its own per-user data manager) and re-register the
|
|
* sovereign://, nostr://, tor:// schemes on it. The old context
|
|
* was torn down by identity_teardown_web_state() above. */
|
|
if (build_context_for_current_user() == NULL) {
|
|
g_printerr("[identity] Failed to build context for new user — "
|
|
"browser will have no web views\n");
|
|
}
|
|
|
|
/* Open a fresh tab for the new user so they don't stare at an
|
|
* empty window after the teardown closed all the previous user's
|
|
* tabs. Uses the new user's per-user new_tab_url setting. */
|
|
const char *url = settings_get()->new_tab_url;
|
|
tab_manager_new_tab(url);
|
|
|
|
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;
|
|
GtkWindow *window = GTK_WINDOW(data);
|
|
|
|
/* Tear down the current user's web state (closes all tabs, destroys
|
|
* the sidebar webviews, wipes cookies/cache/localStorage/service
|
|
* workers/favicons) BEFORE clearing the signer, so the next login
|
|
* starts with a clean web session. See plans/webkit-data-isolation.md. */
|
|
identity_teardown_web_state();
|
|
|
|
if (g_state.signer) {
|
|
nostr_signer_free(g_state.signer);
|
|
g_state.signer = NULL;
|
|
}
|
|
g_state.pubkey_hex[0] = '\0';
|
|
g_state.privkey_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);
|
|
|
|
/* Reset the per-user db tracking so the next login re-opens the
|
|
* new user's browser.db rather than assuming we're already on it. */
|
|
g_current_profile_db[0] = '\0';
|
|
|
|
g_print("[identity] logged out\n");
|
|
|
|
/* Re-show the login dialog so the user (or agent) can log in as a
|
|
* different identity. The dialog runs a nested main loop, so the
|
|
* agent server stays live and an agent can log in via MCP while the
|
|
* dialog is showing. On success, do_login() installs the new signer
|
|
* and switch_to_user_db() opens the new user's profile. */
|
|
if (window != NULL) {
|
|
if (do_login(window) == 0) {
|
|
/* Build a fresh per-user WebKitWebContext for the new
|
|
* identity. The old context was torn down above. For
|
|
* --no-login mode this builds an ephemeral context. */
|
|
if (build_context_for_current_user() != NULL) {
|
|
/* Open a fresh tab for the new user. */
|
|
tab_manager_new_tab(settings_get()->new_tab_url);
|
|
} else {
|
|
g_printerr("[identity] Failed to build context after re-login\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.
|
|
*/
|
|
|
|
/* Hamburger-menu items for sovereign:// internal pages always open in
|
|
* a new tab so the user's current page is preserved. */
|
|
void on_menu_settings(GtkMenuItem *item, gpointer data) {
|
|
(void)item;
|
|
(void)data;
|
|
tab_manager_new_tab("sovereign://settings");
|
|
}
|
|
|
|
void on_menu_profile(GtkMenuItem *item, gpointer data) {
|
|
(void)item;
|
|
(void)data;
|
|
tab_manager_new_tab("sovereign://profile");
|
|
}
|
|
|
|
/* The hamburger-menu "Agent Setup…" item opens the sovereign://agents
|
|
* internal page in a new tab. It 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_manager_new_tab("sovereign://agents");
|
|
}
|
|
|
|
/* The hamburger-menu "FIPS Mesh…" item opens the sovereign://fips
|
|
* internal page in a new tab. It renders the FIPS mesh network
|
|
* status + management UI. */
|
|
void on_menu_fips(GtkMenuItem *item, gpointer data) {
|
|
(void)item;
|
|
(void)data;
|
|
tab_manager_new_tab("sovereign://fips");
|
|
}
|
|
|
|
/* The hamburger-menu "Processes…" item opens the sovereign://processes
|
|
* internal page in a new tab. It renders the process list + per-tab
|
|
* performance diagnostics UI. */
|
|
void on_menu_processes(GtkMenuItem *item, gpointer data) {
|
|
(void)item;
|
|
(void)data;
|
|
tab_manager_new_tab("sovereign://processes");
|
|
}
|
|
|
|
/* ---- 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_OPEN_PROCESSES:
|
|
on_menu_processes(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 close / destroy ----------------------------------------- *
|
|
* The main window uses a delete-event handler to intercept the window
|
|
* manager's close request BEFORE the window is destroyed. This lets us
|
|
* close the main window's tabs cleanly (updating g_tab_count) and keep
|
|
* the app running if auxiliary windows still have tabs. The app only
|
|
* quits when the last window is closed (no tabs remain anywhere).
|
|
*
|
|
* delete-event: runs first. Closes all main-window tabs. If aux windows
|
|
* still have tabs, returns TRUE to suppress destroy and hides the
|
|
* main window. If no tabs remain, returns FALSE to let destroy
|
|
* proceed → on_window_destroy → gtk_main_quit().
|
|
* destroy: runs only when the app is truly shutting down. Performs
|
|
* session save, net_services_shutdown, agent_server_stop, etc.
|
|
*/
|
|
|
|
/* Guard flag: set while on_window_delete_event is closing the main
|
|
* window's tabs. Prevents re-entrancy when tab_manager_close_tab()
|
|
* calls gtk_window_close(g_window) after the last tab is closed, which
|
|
* would re-emit delete-event. */
|
|
static gboolean g_main_window_closing = FALSE;
|
|
|
|
static gboolean on_window_delete_event(GtkWidget *widget,
|
|
GdkEvent *event,
|
|
gpointer data) {
|
|
(void)event;
|
|
(void)data;
|
|
|
|
/* If we're already in the process of closing (re-entrant call from
|
|
* tab_manager_close_tab → gtk_window_close), let the destroy
|
|
* proceed. */
|
|
if (g_main_window_closing) {
|
|
return FALSE;
|
|
}
|
|
g_main_window_closing = TRUE;
|
|
|
|
/* Close all tabs that belong to the main window's notebook. We
|
|
* identify main-window tabs by checking gtk_notebook_page_num on
|
|
* the main notebook. tab_manager_close_tab handles removing from
|
|
* the notebook and the g_tabs array. Re-scan each iteration because
|
|
* closing shifts indices. */
|
|
for (;;) {
|
|
gboolean found = FALSE;
|
|
GtkWidget *main_nb = tab_manager_get_main_notebook();
|
|
if (main_nb == NULL) break;
|
|
/* Close from the highest index down so removal doesn't shift
|
|
* unprocessed indices. Find the highest-index tab in the main
|
|
* notebook and close it. */
|
|
for (int i = tab_manager_count() - 1; i >= 0; i--) {
|
|
tab_info_t *tab = tab_manager_get(i);
|
|
if (tab == NULL || tab->page == NULL) continue;
|
|
if (gtk_notebook_page_num(GTK_NOTEBOOK(main_nb),
|
|
tab->page) >= 0) {
|
|
tab_manager_close_tab(i);
|
|
found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) break;
|
|
}
|
|
|
|
/* If auxiliary windows still have tabs, keep the app running.
|
|
* Suppress the default destroy by returning TRUE, and hide the
|
|
* main window. The aux windows continue independently. */
|
|
if (tab_manager_count() > 0) {
|
|
g_print("[windows] Main window closed but %d tab(s) remain in "
|
|
"other window(s) — keeping app alive.\n",
|
|
tab_manager_count());
|
|
gtk_widget_hide(widget);
|
|
g_main_window_closing = FALSE; /* allow re-opening later */
|
|
return TRUE; /* suppress destroy */
|
|
}
|
|
|
|
/* No tabs left anywhere — let the destroy proceed, which triggers
|
|
* on_window_destroy and quits the app. Keep the guard set so any
|
|
* re-entrant delete-event from the destroy path doesn't re-enter. */
|
|
return FALSE;
|
|
}
|
|
|
|
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';
|
|
/* Carry the privkey (in-memory only) for HMAC d-tag derivation in the
|
|
* bookmarks module. Empty for readonly / nsigner / nip46 methods. */
|
|
strncpy(g_state.privkey_hex, result.identity.privkey_hex, 64);
|
|
g_state.privkey_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).
|
|
* The actual definition is near the top of this file (forward-declared
|
|
* before the menu proxies that reset it on logout). */
|
|
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;
|
|
}
|
|
|
|
/* ---- Per-user WebKit context build helper --------------------------- *
|
|
* Builds a fresh per-user WebKitWebContext (or ephemeral for no-login
|
|
* mode), registers the sovereign://, nostr://, tor:// URI schemes on it,
|
|
* and updates tab_manager so new tabs use the new context. Called from:
|
|
* - main() after login, before creating tabs
|
|
* - app_menu_switch_identity_proxy() after tearing down the old context
|
|
* - app_menu_logout_proxy() after re-login
|
|
*
|
|
* The caller must have torn down the previous context (via
|
|
* web_context_teardown()) and closed all tabs/sidebar webviews first.
|
|
* Returns the new context, or NULL on failure (the caller may fall back
|
|
* to a blank window or exit).
|
|
*/
|
|
static WebKitWebContext *build_context_for_current_user(void) {
|
|
WebKitWebContext *web_ctx;
|
|
if (g_state.pubkey_hex[0] != '\0') {
|
|
web_ctx = web_context_build_for_pubkey(g_state.pubkey_hex);
|
|
} else {
|
|
/* --no-login / read-only-without-pubkey: ephemeral, no on-disk
|
|
* persistence. See plans/webkit-data-isolation.md. */
|
|
web_ctx = web_context_build_ephemeral();
|
|
}
|
|
if (web_ctx == NULL) {
|
|
g_printerr("[main] Failed to build WebKit context\n");
|
|
return NULL;
|
|
}
|
|
|
|
/* Register the sovereign:// URI scheme for the window.nostr bridge.
|
|
* nostr_bridge_set_signer() was already called by the login path,
|
|
* so the bridge will use the current signer. */
|
|
nostr_bridge_register(web_ctx, g_state.signer, g_state.pubkey_hex,
|
|
g_state.readonly);
|
|
|
|
/* Register nostr:// and tor:// entity-page schemes on this context.
|
|
* These are per-context registrations, so they must be re-registered
|
|
* on every fresh context. */
|
|
nostr_scheme_register(web_ctx);
|
|
tor_scheme_register(web_ctx);
|
|
|
|
/* Point tab_manager at the new context so subsequent new tabs (and
|
|
* the sidebar webview) are created from it. */
|
|
tab_manager_set_context(web_ctx);
|
|
|
|
return web_ctx;
|
|
}
|
|
|
|
/* ---- 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, "delete-event",
|
|
G_CALLBACK(on_window_delete_event), NULL);
|
|
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);
|
|
|
|
/* ── Per-user WebKit context (Phase A of webkit-data-isolation.md) ── *
|
|
* Build a fresh WebKitWebContext with a per-user
|
|
* WebKitWebsiteDataManager rooted at
|
|
* ~/.sovereign_browser/profiles/<pubkey>/webkit/ (or an ephemeral
|
|
* data manager for --no-login mode). This gives true per-identity
|
|
* isolation of cookies, cache, localStorage, IndexedDB, service
|
|
* workers, and favicons. The helper also registers the sovereign://,
|
|
* nostr://, tor:// URI schemes on the new context and points
|
|
* tab_manager at it. */
|
|
WebKitWebContext *web_ctx = build_context_for_current_user();
|
|
if (web_ctx == NULL) {
|
|
g_printerr("[main] Cannot continue without a WebKit context.\n");
|
|
agent_server_stop();
|
|
nostr_cleanup();
|
|
cli_args_free(&cli);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
/* Fetch the security manager for the new context — used below to
|
|
* wire the sovereign://security page to the first tab's settings. */
|
|
WebKitSecurityManager *sec_mgr =
|
|
webkit_web_context_get_security_manager(web_ctx);
|
|
|
|
/* Initialize browser-managed/attached network services after the
|
|
* per-user context has been built. net_services_refresh_proxy()
|
|
* now uses web_context_get() so proxy settings apply to the
|
|
* per-user context. */
|
|
net_services_init();
|
|
|
|
/* 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);
|
|
|
|
/* Mark startup complete. From now on, app_set_signer() (called by
|
|
* agent login / switch_identity) will rebuild the per-user web
|
|
* context if it was torn down, since main() won't build it again. */
|
|
g_post_startup = TRUE;
|
|
|
|
/* 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;
|
|
}
|