Compare commits

...
7 Commits
14 changed files with 1148 additions and 178 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ NOSTR_LIB = ./nostr_core_lib/libnostr_core_x64.a
NOSTR_DEPS = -lsecp256k1 -lssl -lcrypto -lcurl -lz -ldl -lpthread -lm
BIN := sovereign_browser
SRC := src/main.c src/key_store.c src/login_dialog.c src/nostr_bridge.c src/nostr_inject.c src/history.c src/settings.c src/tab_manager.c src/session.c src/agent_server.c src/agent_login.c src/agent_snapshot.c src/agent_tools.c src/agent_mcp.c src/cli.c src/qr.c src/db.c src/relay_fetch.c src/bookmarks.c src/shortcuts.c src/settings_sync.c src/search.c
SRC := src/main.c src/key_store.c src/login_dialog.c src/nostr_bridge.c src/nostr_inject.c src/history.c src/settings.c src/tab_manager.c src/session.c src/agent_server.c src/agent_login.c src/agent_snapshot.c src/agent_tools.c src/agent_mcp.c src/cli.c src/qr.c src/db.c src/relay_fetch.c src/bookmarks.c src/shortcuts.c src/settings_sync.c src/search.c src/profile.c
$(BIN): $(SRC) $(NOSTR_LIB)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(NOSTR_LIB) $(LDLIBS) $(NOSTR_DEPS)
+1 -1
View File
@@ -1 +1 @@
0.0.19
0.0.26
+121
View File
@@ -0,0 +1,121 @@
# Per-User Profile Directory System
## Problem
When logging in as a new user (different nsec), the browser:
1. Restores the previous user's tabs (session restore from global SQLite)
2. Shows the previous user's Nostr events on the profile page
3. Shows the previous user's bookmarks, history, and recents
## Solution
Create a per-user profile directory keyed by npub, with each user getting their own SQLite database, session, bookmarks, and history.
## Directory Structure
```
~/.sovereign_browser/
├── identity.json # last used npub (for login dialog default)
├── global.db # global settings (agent server, theme, shortcuts, inspector)
└── profiles/
├── npub1abc.../
│ ├── browser.db # per-user: events, history, session, bookmarks, key_value
│ └── identity.json # this user's nsec/npub
├── npub1def.../
│ ├── browser.db
│ └── identity.json
└── ...
```
## What Stays Global vs Per-User
### Global (in `~/.sovereign_browser/global.db`)
- `agent_server_enabled`, `agent_server_port`, `agent_allowed_origins`, `agent_login_timeout_ms`
- `theme_dark`
- `shortcut.*` (keyboard shortcuts)
- `inspector_x/y/w/h`
- `dev_extras`, `file_access`, `universal_access` (security settings — these are WebKit-level, not per-user)
### Per-User (in `~/.sovereign_browser/profiles/<npub>/browser.db`)
- `restore_session`, `new_tab_url`, `tab_bar_position`, `show_tab_close_buttons`, `middle_click_close`, `ctrl_tab_switch`, `max_tabs`, `tab_drag_reorder`
- `bootstrap_relays`, `search_engine`
- Session (open tabs)
- History
- Bookmarks
- Nostr events (kind 0, 3, 10002)
- key_value table (per-user settings)
## Implementation Steps
### Step 1: Add `db_init_with_path(const char *path)` to db.c
Currently `db_init()` hardcodes `~/.sovereign_browser/browser.db`. Add a variant that takes a path. Keep `db_init()` for the global database.
### Step 2: Add profile directory management
New functions in a `profile.c` module (or add to `key_store.c`):
- `profile_get_dir(const char *npub, char *out, size_t out_sz)` — returns `~/.sovereign_browser/profiles/<npub>/`
- `profile_ensure_dir(const char *npub)` — creates the directory if it doesn't exist
- `profile_get_db_path(const char *npub, char *out, size_t out_sz)` — returns `~/.sovereign_browser/profiles/<npub>/browser.db`
- `profile_get_identity_path(const char *npub, char *out, size_t out_sz)` — returns `~/.sovereign_browser/profiles/<npub>/identity.json`
### Step 3: Split settings into global and per-user
In `settings.c`:
- `settings_load_global()` — loads from global.db (agent server, theme, shortcuts, inspector, security)
- `settings_load_user()` — loads from the per-user browser.db (session, tabs, relays, search)
- `settings_save_global()` — saves global settings to global.db
- `settings_save_user()` — saves per-user settings to browser.db
Or simpler: keep one `settings_load()` / `settings_save()` but have it read from the appropriate database based on which keys are global vs per-user.
### Step 4: Change the login flow in main.c
Current flow:
1. `db_init()` — opens global browser.db
2. `settings_load()` — loads all settings from browser.db
3. `shortcuts_load()` — loads shortcuts from browser.db
4. Login (GTK dialog or agent login or CLI)
5. `relay_fetch()` — fetches events from relays
6. `session_restore()` — restores tabs from browser.db
New flow:
1. `db_init_global()` — opens global.db for global settings
2. `settings_load_global()` — loads global settings (agent server, theme, shortcuts, inspector)
3. `shortcuts_load()` — loads shortcuts from global.db
4. Login — get npub
5. `profile_ensure_dir(npub)` — create `~/.sovereign_browser/profiles/<npub>/`
6. `db_init_user(npub)` — close global.db, open `profiles/<npub>/browser.db`
7. `settings_load_user()` — load per-user settings from browser.db
8. `relay_fetch()` — fetch events from relays into per-user browser.db
9. `session_restore()` — restore tabs from per-user browser.db
### Step 5: Migration
On first run with the new system:
- If `~/.sovereign_browser/browser.db` exists and `~/.sovereign_browser/profiles/` doesn't:
- After login, copy `browser.db` to `profiles/<npub>/browser.db`
- Move global settings from browser.db to global.db
- Keep browser.db as backup (or rename to browser.db.old)
### Step 6: Identity persistence
- `~/.sovereign_browser/identity.json` — stores the last-used npub (not nsec)
- `~/.sovereign_browser/profiles/<npub>/identity.json` — stores the nsec/npub for that profile
- On startup, read the last-used npub from global identity.json to pre-fill the login dialog
## Files to Modify
1. `src/db.c` / `src/db.h` — add `db_init_with_path()`, `db_close()`, `db_init_global()`
2. `src/settings.c` / `src/settings.h` — split into global/per-user load/save
3. `src/main.c` — change the startup flow
4. `src/key_store.c` — update identity persistence to per-profile
5. `src/session.c` — no change (already uses db_session_* which will use the per-user db)
6. `src/shortcuts.c` — load from global.db instead of browser.db
7. New: `src/profile.c` / `src/profile.h` — profile directory management
## Risk Assessment
- **Medium risk** — changes the database initialization flow which affects all data access
- **Migration needed** — existing users need their data moved to per-profile dirs
- **Testing needed** — login with different users, verify data isolation
+130 -13
View File
@@ -24,24 +24,44 @@ static sqlite3 *g_db = NULL;
/* Static buffer for db_kv_get() — valid until the next call. */
static char g_kv_buf[4096];
/* ── Path helper ──────────────────────────────────────────────────── */
/* ── Path helpers ──────────────────────────────────────────────────── */
static int db_path(char *out, size_t out_sz) {
/* ~/.sovereign_browser/ (created if missing). Returns 0 on success. */
static int sb_home_dir(char *out, size_t out_sz) {
const char *home = getenv("HOME");
if (home == NULL || home[0] == '\0') {
return -1;
}
int n = snprintf(out, out_sz, "%s/.sovereign_browser", home);
if (n < 0 || (size_t)n >= out_sz) {
return -1;
}
if (mkdir(out, 0700) != 0 && errno != EEXIST) {
return -1;
}
return 0;
}
/* ~/.sovereign_browser/browser.db (legacy default per-user path). */
static int db_path(char *out, size_t out_sz) {
char dir[512];
int n = snprintf(dir, sizeof(dir), "%s/.sovereign_browser", home);
if (n < 0 || (size_t)n >= sizeof(dir)) {
if (sb_home_dir(dir, sizeof(dir)) != 0) {
return -1;
}
if (mkdir(dir, 0700) != 0 && errno != EEXIST) {
int n = snprintf(out, out_sz, "%s/browser.db", dir);
if (n < 0 || (size_t)n >= out_sz) {
return -1;
}
return 0;
}
n = snprintf(out, out_sz, "%s/browser.db", dir);
/* ~/.sovereign_browser/global.db (global settings + shortcuts). */
static int db_global_path(char *out, size_t out_sz) {
char dir[512];
if (sb_home_dir(dir, sizeof(dir)) != 0) {
return -1;
}
int n = snprintf(out, out_sz, "%s/global.db", dir);
if (n < 0 || (size_t)n >= out_sz) {
return -1;
}
@@ -101,20 +121,29 @@ static const char *SCHEMA_SQL =
/* ── Init / Close ──────────────────────────────────────────────────── */
int db_init(void) {
char path[512];
if (db_path(path, sizeof(path)) != 0) {
g_printerr("[db] Failed to get database path\n");
/*
* Open a SQLite database at the given path with the standard schema.
* Closes any currently-open database first. Returns 0 on success.
*/
int db_init_with_path(const char *path) {
if (path == NULL || path[0] == '\0') {
g_printerr("[db] db_init_with_path: NULL path\n");
return -1;
}
/* Close any currently-open database so we can switch databases. */
if (g_db) {
sqlite3_close(g_db);
g_db = NULL;
}
int rc = sqlite3_open_v2(path, &g_db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
SQLITE_OPEN_FULLMUTEX,
NULL);
if (rc != SQLITE_OK) {
g_printerr("[db] Failed to open database: %s\n",
sqlite3_errmsg(g_db));
g_printerr("[db] Failed to open database '%s': %s\n",
path, g_db ? sqlite3_errmsg(g_db) : "(null)");
if (g_db) {
sqlite3_close(g_db);
g_db = NULL;
@@ -134,7 +163,8 @@ int db_init(void) {
if (err) sqlite3_free(err);
}
/* Create schema. */
/* Create schema (all tables use IF NOT EXISTS, so this is safe for
* both global.db and per-user browser.db). */
rc = sqlite3_exec(g_db, SCHEMA_SQL, NULL, NULL, &err);
if (rc != SQLITE_OK) {
g_printerr("[db] Failed to create schema: %s\n",
@@ -149,6 +179,33 @@ int db_init(void) {
return 0;
}
/*
* Open the global database at ~/.sovereign_browser/global.db.
* Used at startup for global settings + shortcuts, before login.
*/
int db_init_global(void) {
char path[512];
if (db_global_path(path, sizeof(path)) != 0) {
g_printerr("[db] Failed to get global database path\n");
return -1;
}
return db_init_with_path(path);
}
/*
* Open the default per-user database at ~/.sovereign_browser/browser.db.
* Kept for compatibility; the normal flow now uses db_init_with_path()
* with a profile-specific path after login.
*/
int db_init(void) {
char path[512];
if (db_path(path, sizeof(path)) != 0) {
g_printerr("[db] Failed to get database path\n");
return -1;
}
return db_init_with_path(path);
}
void db_close(void) {
if (g_db) {
sqlite3_close(g_db);
@@ -394,6 +451,66 @@ int db_kv_set(const char *key, const char *value) {
return (rc == SQLITE_DONE) ? 0 : -1;
}
/*
* Write a key-value pair to a specific database file, opening a separate
* short-lived connection. This is used to save global settings to
* global.db while the per-user browser.db is the main open database.
* Creates the key_value table if it doesn't exist (safe for a fresh
* global.db). Returns 0 on success, -1 on error.
*/
int db_kv_set_to_file(const char *path, const char *key, const char *value) {
if (path == NULL || key == NULL || value == NULL) return -1;
sqlite3 *db = NULL;
int rc = sqlite3_open_v2(path, &db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |
SQLITE_OPEN_FULLMUTEX,
NULL);
if (rc != SQLITE_OK) {
g_printerr("[db] db_kv_set_to_file: open '%s' failed: %s\n",
path, db ? sqlite3_errmsg(db) : "(null)");
if (db) sqlite3_close(db);
return -1;
}
sqlite3_busy_timeout(db, 5000);
/* Ensure the key_value table exists (harmless if already present). */
char *err = NULL;
rc = sqlite3_exec(db,
"CREATE TABLE IF NOT EXISTS key_value ("
" key TEXT PRIMARY KEY,"
" value TEXT"
");", NULL, NULL, &err);
if (rc != SQLITE_OK) {
g_printerr("[db] db_kv_set_to_file: create table failed: %s\n",
err ? err : "(unknown)");
if (err) sqlite3_free(err);
sqlite3_close(db);
return -1;
}
sqlite3_stmt *stmt = NULL;
rc = sqlite3_prepare_v2(db,
"INSERT OR REPLACE INTO key_value (key, value) VALUES (?, ?);",
-1, &stmt, NULL);
if (rc != SQLITE_OK) {
g_printerr("[db] db_kv_set_to_file: prepare failed: %s\n",
sqlite3_errmsg(db));
sqlite3_close(db);
return -1;
}
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, value, -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
sqlite3_close(db);
return (rc == SQLITE_DONE) ? 0 : -1;
}
const char *db_kv_get(const char *key) {
if (g_db == NULL || key == NULL) return NULL;
+31 -3
View File
@@ -28,14 +28,33 @@ extern "C" {
/*
* Initialize the database at ~/.sovereign_browser/browser.db.
* Creates tables and indexes if they don't exist.
* Call once at startup (after settings_load()).
* Kept for compatibility; prefer db_init_with_path() for per-user dbs.
*
* Returns 0 on success, -1 on error.
*/
int db_init(void);
/*
* Close the database. Call at shutdown.
* Initialize the global database at ~/.sovereign_browser/global.db.
* Used at startup for global settings + shortcuts, before login.
* Creates tables and indexes if they don't exist.
*
* Returns 0 on success, -1 on error.
*/
int db_init_global(void);
/*
* Open a SQLite database at the given path with the standard schema.
* Closes any currently-open database first (so you can switch from
* global.db to a per-user browser.db after login).
*
* Returns 0 on success, -1 on error.
*/
int db_init_with_path(const char *path);
/*
* Close the database. Call at shutdown, or before switching to a
* different database via db_init_with_path().
*/
void db_close(void);
@@ -79,11 +98,20 @@ int db_count_events(const char *pubkey_hex, int kind);
/* ── Key-Value store ───────────────────────────────────────────────── */
/*
* Set a key-value pair (upsert).
* Set a key-value pair (upsert) in the currently-open database.
* Returns 0 on success, -1 on error.
*/
int db_kv_set(const char *key, const char *value);
/*
* Set a key-value pair in a specific database file, opening a separate
* short-lived connection. Used to save global settings to global.db
* while the per-user browser.db is the main open database. Creates the
* key_value table if it doesn't exist.
* Returns 0 on success, -1 on error.
*/
int db_kv_set_to_file(const char *path, const char *key, const char *value);
/*
* Get a value by key.
* Returns a pointer to the value string, or NULL if not found.
+103
View File
@@ -9,9 +9,14 @@
* in-memory identity struct
* - key_store_delete_legacy_file() — defensively delete any
* identity.json left by a previous version that persisted keys
* - key_store_save_profile_identity() — save public identity info
* (method, pubkey_hex) to the per-profile identity.json
* - key_store_load_profile_identity() — load public identity info
* from the per-profile identity.json
*/
#include "key_store.h"
#include "profile.h"
#include <stdio.h>
#include <stdlib.h>
@@ -19,6 +24,8 @@
#include <unistd.h>
#include <errno.h>
#include "../nostr_core_lib/cjson/cJSON.h"
#include "nostr_core/nostr_core.h"
#include "nostr_core/nip006.h"
#include "nostr_core/nip019.h"
@@ -109,3 +116,99 @@ int key_store_delete_legacy_file(void) {
}
return 0;
}
/* ── Per-profile identity persistence ──────────────────────────────── *
* The per-profile identity.json stores public identity information
* (method, pubkey_hex) so the browser can display profile info without
* requiring the user to log in first. Private keys are NEVER stored.
*/
int key_store_save_profile_identity(const char *pubkey_hex,
key_store_method_t method) {
if (pubkey_hex == NULL || pubkey_hex[0] == '\0') {
return -1;
}
char path[512];
profile_get_identity_path(pubkey_hex, path, sizeof(path));
if (path[0] == '\0') {
return -1;
}
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "pubkey_hex", pubkey_hex);
cJSON_AddNumberToObject(root, "method", (double)method);
/* Method name for human readability. */
const char *method_name = "none";
switch (method) {
case KEY_STORE_METHOD_NONE: method_name = "none"; break;
case KEY_STORE_METHOD_LOCAL: method_name = "local"; break;
case KEY_STORE_METHOD_SEED: method_name = "seed"; break;
case KEY_STORE_METHOD_READONLY: method_name = "readonly"; break;
case KEY_STORE_METHOD_NIP46: method_name = "nip46"; break;
case KEY_STORE_METHOD_NSIGNER: method_name = "nsigner"; break;
}
cJSON_AddStringToObject(root, "method_name", method_name);
char *json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (json == NULL) {
return -1;
}
FILE *f = fopen(path, "w");
if (f == NULL) {
free(json);
return -1;
}
fputs(json, f);
fputc('\n', f);
fclose(f);
free(json);
return 0;
}
int key_store_load_profile_identity(const char *pubkey_hex,
key_store_method_t *method_out) {
if (pubkey_hex == NULL || pubkey_hex[0] == '\0') {
return -1;
}
char path[512];
profile_get_identity_path(pubkey_hex, path, sizeof(path));
if (path[0] == '\0') {
return -1;
}
FILE *f = fopen(path, "r");
if (f == NULL) {
return -1;
}
char buf[1024];
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
fclose(f);
buf[n] = '\0';
cJSON *root = cJSON_Parse(buf);
if (root == NULL) {
return -1;
}
int rc = -1;
cJSON *pk = cJSON_GetObjectItemCaseSensitive(root, "pubkey_hex");
if (cJSON_IsString(pk) && strcmp(pk->valuestring, pubkey_hex) == 0) {
if (method_out) {
cJSON *m = cJSON_GetObjectItemCaseSensitive(root, "method");
if (cJSON_IsNumber(m)) {
*method_out = (key_store_method_t)m->valuedouble;
}
}
rc = 0;
}
cJSON_Delete(root);
return rc;
}
+18
View File
@@ -69,6 +69,24 @@ nostr_signer_t *key_store_create_signer(const key_store_identity_t *identity);
*/
int key_store_delete_legacy_file(void);
/*
* Save public identity info (method, pubkey_hex) to the per-profile
* identity.json at ~/.sovereign_browser/profiles/<pubkey>/identity.json.
* Private keys are NEVER stored — only public information for display.
* Returns 0 on success, -1 on error.
*/
int key_store_save_profile_identity(const char *pubkey_hex,
key_store_method_t method);
/*
* Load public identity info from the per-profile identity.json.
* pubkey_hex — the expected pubkey (must match the file's pubkey)
* method_out — if non-NULL, receives the stored login method
* Returns 0 on success, -1 if not found or pubkey mismatch.
*/
int key_store_load_profile_identity(const char *pubkey_hex,
key_store_method_t *method_out);
#ifdef __cplusplus
}
#endif
+145 -15
View File
@@ -47,6 +47,7 @@
#include "agent_login.h"
#include "cli.h"
#include "db.h"
#include "profile.h"
#include "relay_fetch.h"
#include "bookmarks.h"
#include "nostr_core/nostr_core.h"
@@ -65,6 +66,9 @@ typedef struct {
} 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 */
@@ -85,6 +89,14 @@ void app_set_signer(nostr_signer_t *signer, const char *pubkey_hex,
/* Update modules that hold a signer reference. */
settings_sync_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) {
@@ -130,6 +142,13 @@ void app_menu_switch_identity_proxy(GtkMenuItem *item, gpointer data) {
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);
}
@@ -365,8 +384,17 @@ static void on_window_destroy(GtkWidget *widget, gpointer data) {
(void)widget;
(void)data;
/* Save the session before cleanup. */
session_save();
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 the agent server. */
agent_server_stop();
@@ -428,6 +456,78 @@ static int do_login(GtkWindow *parent) {
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) {
@@ -454,20 +554,34 @@ int main(int argc, char **argv) {
gtk_init(&argc, &argv);
/* Defensively delete any legacy identity.json from a previous version
* that persisted private keys to disk. Keys are now in-memory only. */
key_store_delete_legacy_file();
/* 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 SQLite database first — settings and history are
* stored there. */
db_init();
/* 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();
/* Load settings from the database (key_value table). */
settings_load();
/* 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. Must come after
* settings_load() because shortcuts_lookup() checks the master
* ctrl_tab_switch toggle. */
/* 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
@@ -590,10 +704,26 @@ int main(int argc, char **argv) {
}
}
/* ── 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 SQLite database.
* The thread frees the pubkey copy when done. */
* 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);
+77 -83
View File
@@ -473,6 +473,7 @@ static const char *sovereign_page_css(void) {
" border-radius: 4px; }\n"
" .theme-toggle { position: fixed; top: 20px; right: 20px;\n"
" display: flex; align-items: center; gap: 8px; cursor: pointer;\n"
" text-decoration: none; user-select: none;\n"
" font-size: 12px; color: var(--muted); user-select: none;\n"
" border: 1px solid var(--border); border-radius: var(--radius);\n"
" padding: 4px 10px; background: var(--bg); z-index: 10; }\n"
@@ -506,12 +507,7 @@ static char *sovereign_page_head(const char *title) {
/* Build the theme-toggle button + closing </body></html>.
* Returns a newly-allocated string (g_free). */
static char *sovereign_page_foot(void) {
return g_strdup_printf(
"<div class='theme-toggle' onclick='document.location.href="
"\"sovereign://settings/set?feature=theme_dark\"'>"
"%s &#9728;&#9790;</div>\n"
"</body></html>\n",
settings_get()->theme_dark ? "Day" : "Night");
return g_strdup("</body></html>\n");
}
/* ── Settings page ─────────────────────────────────────────────── */
@@ -585,35 +581,29 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
/* Build the keyboard shortcuts section HTML. One row per action. */
GString *shortcuts_html = g_string_new(NULL);
g_string_append(shortcuts_html,
"<h2>Keyboard Shortcuts</h2>\n"
"<p class='note'>Click <b>Change</b> then press the key combination "
"you want. Press <b>Escape</b> to cancel.</p>\n");
"<h2>Keyboard Shortcuts</h2>\n");
for (int i = 0; i < SHORTCUT_COUNT; i++) {
const shortcut_meta_t *m = shortcuts_meta((shortcut_action_t)i);
const char *accel = shortcuts_get((shortcut_action_t)i);
char *label_esc = g_markup_escape_text(m->label, -1);
char *desc_esc = g_markup_escape_text(m->desc, -1);
char *accel_esc = g_markup_escape_text(accel ? accel : "", -1);
g_string_append_printf(shortcuts_html,
"<div class='field shortcut-row' data-action='%s' data-accel='%s'>\n"
" <div><div class='setting-name'>%s</div>\n"
" <div class='setting-desc'>%s</div></div>\n"
" <div><div class='setting-name'>%s</div></div>\n"
" <div><span class='shortcut-display' id='sc_%s'>%s</span>\n"
" <button class='save-btn sc-change' data-action='%s'>Change</button>\n"
" <button class='save-btn sc-reset' data-action='%s'>Reset</button></div>\n"
"</div>\n",
m->id, accel ? accel : "",
label_esc, desc_esc,
label_esc,
m->id, accel_esc,
m->id, m->id);
g_free(label_esc);
g_free(desc_esc);
g_free(accel_esc);
}
g_string_append(shortcuts_html,
"<div class='field'>"
"<div><div class='setting-name'>Reset all shortcuts</div>"
"<div class='setting-desc'>Restore all bindings to defaults</div></div>"
"<div><div class='setting-name'>Reset all shortcuts</div></div>"
"<div><button class='save-btn' id='sc-reset-all'>Reset All</button></div>"
"</div>\n");
@@ -627,29 +617,25 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
"<h2>Appearance</h2>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Dark mode (Night)</div>\n"
" <div class='setting-desc'>Black background, white text. Toggle for Day mode.</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"theme_dark\")'></div>\n"
" <div><div class='setting-name'>Dark mode (Night)</div></div>\n"
" <div class='toggle %s' id='toggle_theme_dark' onclick='toggle(\"theme_dark\")'></div>\n"
"</div>\n"
"\n"
"<h2>Tabs</h2>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Restore session on startup</div>\n"
" <div class='setting-desc'>Reopen tabs from the previous session</div></div>\n"
" <div><div class='setting-name'>Restore session on startup</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"restore_session\")'></div>\n"
"</div>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>New tab URL</div>\n"
" <div class='setting-desc'>Page loaded when opening a new tab</div></div>\n"
" <div><div class='setting-name'>New tab URL</div></div>\n"
" <div><input type='text' id='new_tab_url' value='%s'>\n"
" <button class='save-btn' onclick=\"save('new_tab_url')\">Save</button></div>\n"
"</div>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Tab bar position</div>\n"
" <div class='setting-desc'>Where the tab strip appears</div></div>\n"
" <div><div class='setting-name'>Tab bar position</div></div>\n"
" <div><select id='tab_bar_position'>\n"
" <option value='top'%s>Top</option>\n"
" <option value='bottom'%s>Bottom</option>\n"
@@ -660,32 +646,27 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Show tab close buttons</div>\n"
" <div class='setting-desc'>Per-tab close button in the tab strip</div></div>\n"
" <div><div class='setting-name'>Show tab close buttons</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"show_tab_close_buttons\")'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Middle-click to close tab</div>\n"
" <div class='setting-desc'>Middle-click on a tab closes it</div></div>\n"
" <div><div class='setting-name'>Middle-click to close tab</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"middle_click_close\")'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Ctrl+Tab to switch tabs</div>\n"
" <div class='setting-desc'>Cycle tabs with Ctrl+Tab / Ctrl+Shift+Tab</div></div>\n"
" <div><div class='setting-name'>Ctrl+Tab to switch tabs</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"ctrl_tab_switch\")'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Allow drag to reorder tabs</div>\n"
" <div class='setting-desc'>Drag tabs in the strip to reorder them</div></div>\n"
" <div><div class='setting-name'>Allow drag to reorder tabs</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"tab_drag_reorder\")'></div>\n"
"</div>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Maximum tabs</div>\n"
" <div class='setting-desc'>Hard limit on simultaneous tabs</div></div>\n"
" <div><div class='setting-name'>Maximum tabs</div></div>\n"
" <div><input type='number' id='max_tabs' value='%d' min='1' max='999'>\n"
" <button class='save-btn' onclick=\"save('max_tabs')\">Save</button></div>\n"
"</div>\n"
@@ -695,38 +676,31 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
"<h2>Agent Server</h2>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Enable agent server</div>\n"
" <div class='setting-desc'>MCP server for automation (takes effect on next launch)</div></div>\n"
" <div><div class='setting-name'>Enable agent server</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"agent_server_enabled\")'></div>\n"
"</div>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Agent server port</div>\n"
" <div class='setting-desc'>TCP port for the MCP server (next launch)</div></div>\n"
" <div><div class='setting-name'>Agent server port</div></div>\n"
" <div><input type='number' id='agent_server_port' value='%d' min='1' max='65535'>\n"
" <button class='save-btn' onclick=\"save('agent_server_port')\">Save</button></div>\n"
"</div>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Allowed origins</div>\n"
" <div class='setting-desc'>Comma-separated CORS origins (* for any)</div></div>\n"
" <div><div class='setting-name'>Allowed origins</div></div>\n"
" <div><input type='text' id='agent_allowed_origins' value='%s'>\n"
" <button class='save-btn' onclick=\"save('agent_allowed_origins')\">Save</button></div>\n"
"</div>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Login timeout (ms)</div>\n"
" <div class='setting-desc'>Wait for agent login before GTK dialog</div></div>\n"
" <div><div class='setting-name'>Login timeout (ms)</div></div>\n"
" <div><input type='number' id='agent_login_timeout_ms' value='%d' min='0' max='300000'>\n"
" <button class='save-btn' onclick=\"save('agent_login_timeout_ms')\">Save</button></div>\n"
"</div>\n"
"\n"
"<h2>Bootstrap Relays</h2>\n"
"<p class='note'>Relays queried after login to fetch your profile (kind 0),\n"
"contacts (kind 3), and relay list (kind 10002). One wss:// URL per line.</p>\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Relay URLs</div>\n"
" <div class='setting-desc'>One wss:// URL per line</div></div>\n"
" <div><div class='setting-name'>Relay URLs</div></div>\n"
" <div><textarea id='bootstrap_relays' rows='4' cols='40'>%s</textarea>\n"
" <button class='save-btn' onclick=\"save('bootstrap_relays')\">Save</button></div>\n"
"</div>\n"
@@ -734,8 +708,7 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
"<h2>Search</h2>\n"
"\n"
"<div class='field'>\n"
" <div><div class='setting-name'>Search engine</div>\n"
" <div class='setting-desc'>Engine used when typing a query in the URL bar</div></div>\n"
" <div><div class='setting-name'>Search engine</div></div>\n"
" <div><select id='search_engine'>\n"
"%s"
" </select>\n"
@@ -743,37 +716,29 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
"</div>\n"
"\n"
"<h2>Security</h2>\n"
"<p class='note'>The 'reckless browser' thesis: identity and transport are handled\n"
"at a different layer (Nostr keys + FIPS mesh), so the browser's own security\n"
"sandbox is deliberately stripped. Toggle features on if you need them.</p>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Developer Tools (Inspector)</div>\n"
" <div class='setting-desc'>Enable WebKit Web Inspector for JS debugging</div></div>\n"
" <div><div class='setting-name'>Developer Tools (Inspector)</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"dev_extras\")'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>File Access from file://</div>\n"
" <div class='setting-desc'>Allow file:// pages to access other file:// resources</div></div>\n"
" <div><div class='setting-name'>File Access from file://</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"file_access\")'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>Universal Access from file://</div>\n"
" <div class='setting-desc'>Allow file:// pages to make cross-origin requests (needed for sovereign:// bridge)</div></div>\n"
" <div><div class='setting-name'>Universal Access from file://</div></div>\n"
" <div class='toggle %s' onclick='toggle(\"universal_access\")'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>CORS Enforcement</div>\n"
" <div class='setting-desc'>Block cross-origin requests without CORS headers (currently always off)</div></div>\n"
" <div><div class='setting-name'>CORS Enforcement</div></div>\n"
" <div class='toggle off' style='opacity:0.5'></div>\n"
"</div>\n"
"\n"
"<div class='setting'>\n"
" <div><div class='setting-name'>TLS Certificate Check</div>\n"
" <div class='setting-desc'>Reject invalid/self-signed certificates (currently always off — FIPS uses Noise IK)</div></div>\n"
" <div><div class='setting-name'>TLS Certificate Check</div></div>\n"
" <div class='toggle off' style='opacity:0.5'></div>\n"
"</div>\n"
"\n"
@@ -786,33 +751,60 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
" s.textContent = msg;\n"
" }\n"
" function toggle(feature) {\n"
" fetch('sovereign://settings/set?feature=' + feature)\n"
" .then(r => r.json())\n"
" .then(d => {\n"
" if (d.error) {\n"
" showStatus('err', d.message);\n"
" } else {\n"
" /* For theme_dark, toggle the CSS class and the toggle button\n"
" * instantly for immediate visual feedback, then save via async\n"
" * XHR (no reload needed). */\n"
" if (feature === 'theme_dark') {\n"
" var h = document.documentElement;\n"
" var isDark = h.classList.contains('dark');\n"
" if (isDark) h.classList.remove('dark');\n"
" else h.classList.add('dark');\n"
" /* Flip the toggle button visual state */\n"
" var btn = document.getElementById('toggle_theme_dark');\n"
" if (btn) {\n"
" btn.classList.remove('on', 'off');\n"
" btn.classList.add(isDark ? 'off' : 'on');\n"
" }\n"
" var x = new XMLHttpRequest();\n"
" x.open('GET', 'sovereign://settings/set?feature=' + feature, true);\n"
" x.send();\n"
" showStatus('ok', 'theme_dark: ' + (isDark ? 'OFF' : 'ON'));\n"
" return;\n"
" }\n"
" /* For other features, use async XHR and reload to reflect changes. */\n"
" var x = new XMLHttpRequest();\n"
" x.open('GET', 'sovereign://settings/set?feature=' + feature, true);\n"
" x.onreadystatechange = function() {\n"
" if (x.readyState !== 4) return;\n"
" try {\n"
" var d = JSON.parse(x.responseText);\n"
" if (d.error) { showStatus('err', d.message); }\n"
" else {\n"
" showStatus('ok', d.key + ': ' + (d.value ? 'ON' : 'OFF'));\n"
" setTimeout(() => location.reload(), 500);\n"
" setTimeout(function() { location.reload(); }, 300);\n"
" }\n"
" })\n"
" .catch(e => showStatus('err', 'Error: ' + e.message));\n"
" } catch(e) { showStatus('err', 'Error: ' + e.message); }\n"
" };\n"
" x.send();\n"
" }\n"
" function save(key) {\n"
" var el = document.getElementById(key);\n"
" var val = el ? el.value : '';\n"
" fetch('sovereign://settings/set?key=' + encodeURIComponent(key)\n"
" + '&value=' + encodeURIComponent(val))\n"
" .then(r => r.json())\n"
" .then(d => {\n"
" if (d.error) {\n"
" showStatus('err', d.message);\n"
" } else {\n"
" var x = new XMLHttpRequest();\n"
" x.open('GET', 'sovereign://settings/set?key=' + encodeURIComponent(key)\n"
" + '&value=' + encodeURIComponent(val), true);\n"
" x.onreadystatechange = function() {\n"
" if (x.readyState !== 4) return;\n"
" try {\n"
" var d = JSON.parse(x.responseText);\n"
" if (d.error) { showStatus('err', d.message); }\n"
" else {\n"
" showStatus('ok', d.key + ' = ' + d.value);\n"
" if (d.reload) setTimeout(() => location.reload(), 500);\n"
" if (d.reload) setTimeout(function() { location.reload(); }, 300);\n"
" }\n"
" })\n"
" .catch(e => showStatus('err', 'Error: ' + e.message));\n"
" } catch(e) { showStatus('err', 'Error: ' + e.message); }\n"
" };\n"
" x.send();\n"
" }\n"
"\n"
" /* ── Keyboard shortcut capture ─────────────────────────── */\n"
@@ -996,7 +988,8 @@ static void handle_settings_page(WebKitURISchemeRequest *request) {
search_engine_options->str,
dev_extras ? "on" : "off",
file_access ? "on" : "off",
universal_access ? "on" : "off"
universal_access ? "on" : "off",
foot
);
respond_html(request, html);
@@ -1069,7 +1062,8 @@ static void handle_settings_set(WebKitURISchemeRequest *request,
bs->theme_dark = !bs->theme_dark;
new_state = bs->theme_dark;
settings_save();
reload = 1;
settings_sync_publish();
reload = 0; /* Don't reload — the JS handles the visual toggle */
} else if (strcmp(feature, "agent_server_enabled") == 0) {
bs->agent_server_enabled = !bs->agent_server_enabled;
new_state = bs->agent_server_enabled;
+239
View File
@@ -0,0 +1,239 @@
/*
* profile.c — per-user profile directory management for sovereign_browser
*
* Each Nostr identity (identified by its 64-char hex pubkey) gets its own
* profile directory under ~/.sovereign_browser/profiles/<pubkey_hex>/.
*/
#include "profile.h"
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "../nostr_core_lib/cjson/cJSON.h"
/* ── Internal helpers ──────────────────────────────────────────────── */
/* Get ~/.sovereign_browser/ (created if missing). Returns 0 on success. */
static int sb_home_dir(char *out, size_t out_sz) {
const char *home = getenv("HOME");
if (home == NULL || home[0] == '\0') {
return -1;
}
int n = snprintf(out, out_sz, "%s/.sovereign_browser", home);
if (n < 0 || (size_t)n >= out_sz) {
return -1;
}
if (mkdir(out, 0700) != 0 && errno != EEXIST) {
return -1;
}
return 0;
}
/* Max path length we use for home-derived paths. The home directory
* can be long, and we append "/.sovereign_browser/profiles/<64-hex>/".
* 600 bytes is generous enough for any realistic HOME path. */
#define SB_HOME_BUF_SZ 600
/* mkdir -p style: create a directory and all parent directories. */
static int mkdir_p(const char *path, mode_t mode) {
char tmp[512];
int n = snprintf(tmp, sizeof(tmp), "%s", path);
if (n < 0 || (size_t)n >= sizeof(tmp)) {
return -1;
}
/* Remove trailing slash if present. */
size_t len = strlen(tmp);
if (len > 0 && tmp[len - 1] == '/') {
tmp[len - 1] = '\0';
}
/* Walk the path and create each component. */
for (char *p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = '\0';
if (mkdir(tmp, mode) != 0 && errno != EEXIST) {
return -1;
}
*p = '/';
}
}
if (mkdir(tmp, mode) != 0 && errno != EEXIST) {
return -1;
}
return 0;
}
/* ── Public API ────────────────────────────────────────────────────── */
void profile_get_dir(const char *pubkey_hex, char *out, size_t out_sz) {
if (pubkey_hex == NULL || out == NULL || out_sz == 0) {
if (out && out_sz > 0) out[0] = '\0';
return;
}
char home[600];
if (sb_home_dir(home, sizeof(home)) != 0) {
if (out_sz > 0) out[0] = '\0';
return;
}
int n = snprintf(out, out_sz, "%s/profiles/%s/", home, pubkey_hex);
if (n < 0 || (size_t)n >= out_sz) {
if (out_sz > 0) out[0] = '\0';
}
}
int profile_ensure_dir(const char *pubkey_hex) {
if (pubkey_hex == NULL || pubkey_hex[0] == '\0') {
return -1;
}
char dir[SB_HOME_BUF_SZ];
profile_get_dir(pubkey_hex, dir, sizeof(dir));
if (dir[0] == '\0') {
return -1;
}
return mkdir_p(dir, 0700);
}
void profile_get_db_path(const char *pubkey_hex, char *out, size_t out_sz) {
if (pubkey_hex == NULL || out == NULL || out_sz == 0) {
if (out && out_sz > 0) out[0] = '\0';
return;
}
char dir[SB_HOME_BUF_SZ];
profile_get_dir(pubkey_hex, dir, sizeof(dir));
if (dir[0] == '\0') {
if (out_sz > 0) out[0] = '\0';
return;
}
int n = snprintf(out, out_sz, "%sbrowser.db", dir);
if (n < 0 || (size_t)n >= out_sz) {
if (out_sz > 0) out[0] = '\0';
}
}
void profile_get_identity_path(const char *pubkey_hex, char *out, size_t out_sz) {
if (pubkey_hex == NULL || out == NULL || out_sz == 0) {
if (out && out_sz > 0) out[0] = '\0';
return;
}
char dir[SB_HOME_BUF_SZ];
profile_get_dir(pubkey_hex, dir, sizeof(dir));
if (dir[0] == '\0') {
if (out_sz > 0) out[0] = '\0';
return;
}
int n = snprintf(out, out_sz, "%sidentity.json", dir);
if (n < 0 || (size_t)n >= out_sz) {
if (out_sz > 0) out[0] = '\0';
}
}
void profile_get_global_identity_path(char *out, size_t out_sz) {
if (out == NULL || out_sz == 0) {
return;
}
char home[SB_HOME_BUF_SZ];
if (sb_home_dir(home, sizeof(home)) != 0) {
out[0] = '\0';
return;
}
int n = snprintf(out, out_sz, "%s/identity.json", home);
if (n < 0 || (size_t)n >= out_sz) {
if (out_sz > 0) out[0] = '\0';
}
}
/* ── Last-used pubkey persistence (global identity.json) ───────────── */
int profile_save_last_pubkey(const char *pubkey_hex) {
char path[SB_HOME_BUF_SZ];
profile_get_global_identity_path(path, sizeof(path));
if (path[0] == '\0') {
return -1;
}
cJSON *root = cJSON_CreateObject();
if (pubkey_hex && pubkey_hex[0]) {
cJSON_AddStringToObject(root, "pubkey_hex", pubkey_hex);
} else {
cJSON_AddNullToObject(root, "pubkey_hex");
}
char *json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (json == NULL) {
return -1;
}
FILE *f = fopen(path, "w");
if (f == NULL) {
free(json);
return -1;
}
fputs(json, f);
fputc('\n', f);
fclose(f);
free(json);
g_print("[profile] Saved last-used pubkey: %s\n",
(pubkey_hex && pubkey_hex[0]) ? pubkey_hex : "(cleared)");
return 0;
}
int profile_load_last_pubkey(char *out, size_t out_sz) {
if (out == NULL || out_sz < 65) {
return -1;
}
char path[SB_HOME_BUF_SZ];
profile_get_global_identity_path(path, sizeof(path));
if (path[0] == '\0') {
return -1;
}
FILE *f = fopen(path, "r");
if (f == NULL) {
return -1;
}
/* Read the file (small JSON, a few hundred bytes at most). */
char buf[1024];
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
fclose(f);
buf[n] = '\0';
cJSON *root = cJSON_Parse(buf);
if (root == NULL) {
return -1;
}
cJSON *pk = cJSON_GetObjectItemCaseSensitive(root, "pubkey_hex");
const char *hex = cJSON_IsString(pk) ? pk->valuestring : NULL;
int rc = -1;
if (hex && strlen(hex) == 64) {
snprintf(out, out_sz, "%s", hex);
rc = 0;
}
cJSON_Delete(root);
return rc;
}
+77
View File
@@ -0,0 +1,77 @@
/*
* profile.h — per-user profile directory management for sovereign_browser
*
* Each Nostr identity (identified by its 64-char hex pubkey) gets its own
* profile directory under ~/.sovereign_browser/profiles/<pubkey_hex>/
* containing a per-user browser.db and identity.json.
*
* Global settings (agent server, theme, shortcuts, inspector geometry)
* live in ~/.sovereign_browser/global.db and are shared across all users.
*/
#ifndef PROFILE_H
#define PROFILE_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* Get the profile directory path for a given hex pubkey.
* pubkey_hex — 64-char hex pubkey (no leading "0x")
* out — output buffer
* out_sz — size of output buffer
* Writes "~/.sovereign_browser/profiles/<pubkey_hex>/" to out.
*/
void profile_get_dir(const char *pubkey_hex, char *out, size_t out_sz);
/*
* Create the profile directory for the given hex pubkey if it doesn't
* already exist (mkdir -p style — creates parent dirs as needed).
* pubkey_hex — 64-char hex pubkey
* Returns 0 on success, -1 on error.
*/
int profile_ensure_dir(const char *pubkey_hex);
/*
* Get the per-user browser.db path for a given hex pubkey.
* Writes "~/.sovereign_browser/profiles/<pubkey_hex>/browser.db" to out.
*/
void profile_get_db_path(const char *pubkey_hex, char *out, size_t out_sz);
/*
* Get the per-user identity.json path for a given hex pubkey.
* Writes "~/.sovereign_browser/profiles/<pubkey_hex>/identity.json" to out.
*/
void profile_get_identity_path(const char *pubkey_hex, char *out, size_t out_sz);
/*
* Get the global identity.json path (stores the last-used pubkey_hex so
* the login dialog can default to the right profile).
* Writes "~/.sovereign_browser/identity.json" to out.
*/
void profile_get_global_identity_path(char *out, size_t out_sz);
/*
* Save the last-used pubkey_hex to the global identity.json.
* pubkey_hex — 64-char hex pubkey, or NULL/empty to clear
* Returns 0 on success, -1 on error.
*/
int profile_save_last_pubkey(const char *pubkey_hex);
/*
* Read the last-used pubkey_hex from the global identity.json.
* out — output buffer (must be at least 65 bytes)
* out_sz — size of output buffer
* Returns 0 on success (out filled with hex pubkey), -1 if not found or
* error (out is left unchanged).
*/
int profile_load_last_pubkey(char *out, size_t out_sz);
#ifdef __cplusplus
}
#endif
#endif /* PROFILE_H */
+167 -55
View File
@@ -15,6 +15,37 @@
#include <stdlib.h>
#include <string.h>
#include <strings.h> /* strcasecmp */
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
/* ── Global.db path helper ─────────────────────────────────────────── *
* Returns the path to ~/.sovereign_browser/global.db (creating the
* directory if needed). Used by settings_save_global() to write global
* settings to global.db via db_kv_set_to_file(), even when the per-user
* browser.db is the main open database.
*/
static void global_db_path(char *out, size_t out_sz) {
const char *home = getenv("HOME");
if (home == NULL || home[0] == '\0') {
if (out_sz > 0) out[0] = '\0';
return;
}
char dir[512];
int n = snprintf(dir, sizeof(dir), "%s/.sovereign_browser", home);
if (n < 0 || (size_t)n >= sizeof(dir)) {
if (out_sz > 0) out[0] = '\0';
return;
}
if (mkdir(dir, 0700) != 0 && errno != EEXIST) {
if (out_sz > 0) out[0] = '\0';
return;
}
n = snprintf(out, out_sz, "%s/global.db", dir);
if (n < 0 || (size_t)n >= out_sz) {
if (out_sz > 0) out[0] = '\0';
}
}
/* ── Global singleton ─────────────────────────────────────────────── */
@@ -23,7 +54,7 @@ static browser_settings_t g_settings;
/* ── Defaults ─────────────────────────────────────────────────────── */
static void settings_set_defaults(browser_settings_t *s) {
s->restore_session = TRUE;
s->restore_session = FALSE;
snprintf(s->new_tab_url, sizeof(s->new_tab_url), "%s",
SETTINGS_NEW_TAB_URL_DEFAULT);
s->tab_bar_position = GTK_POS_TOP;
@@ -96,13 +127,80 @@ static const char *position_to_string(int pos) {
}
}
/* ── Load / Save (SQLite key_value table) ─────────────────────────── */
/* ── Load / Save (SQLite key_value table) ─────────────────────────── *
* Settings are split into two groups:
*
* GLOBAL (stored in ~/.sovereign_browser/global.db):
* agent_server_enabled, agent_server_port, agent_allowed_origins,
* agent_login_timeout_ms, theme_dark, inspector_x/y/w/h
*
* PER-USER (stored in ~/.sovereign_browser/profiles/<pubkey>/browser.db):
* restore_session, new_tab_url, tab_bar_position,
* show_tab_close_buttons, middle_click_close, ctrl_tab_switch,
* max_tabs, tab_drag_reorder, bootstrap_relays, search_engine
*
* The settings struct is a single in-memory singleton. Global settings
* are loaded once at startup (from global.db) and kept in memory. Per-user
* settings are loaded after login (from the per-user browser.db). Both
* groups are saved when the user changes settings on the settings page.
*/
void settings_load(void) {
settings_set_defaults(&g_settings);
void settings_load_global(void) {
/* Read global settings from the currently-open database (global.db).
* Defaults must already be set (call settings_set_defaults first, or
* call settings_load_user() which sets defaults). */
const char *val;
/* Read each setting from the key_value table. If a key is missing
* (NULL), the default is kept. */
val = db_kv_get("agent_server_enabled");
if (val) g_settings.agent_server_enabled = parse_bool(val, g_settings.agent_server_enabled);
val = db_kv_get("agent_server_port");
if (val) {
g_settings.agent_server_port = parse_int(val, g_settings.agent_server_port);
if (g_settings.agent_server_port < 1) g_settings.agent_server_port = SETTINGS_AGENT_PORT_DEFAULT;
}
val = db_kv_get("agent_allowed_origins");
if (val) snprintf(g_settings.agent_allowed_origins, sizeof(g_settings.agent_allowed_origins), "%s", val);
val = db_kv_get("agent_login_timeout_ms");
if (val) {
g_settings.agent_login_timeout_ms = parse_int(val, g_settings.agent_login_timeout_ms);
if (g_settings.agent_login_timeout_ms < 0) g_settings.agent_login_timeout_ms = 0;
}
val = db_kv_get("theme_dark");
if (val) g_settings.theme_dark = parse_bool(val, g_settings.theme_dark);
val = db_kv_get("inspector_x");
if (val) g_settings.inspector_x = parse_int(val, g_settings.inspector_x);
val = db_kv_get("inspector_y");
if (val) g_settings.inspector_y = parse_int(val, g_settings.inspector_y);
val = db_kv_get("inspector_w");
if (val) g_settings.inspector_w = parse_int(val, g_settings.inspector_w);
val = db_kv_get("inspector_h");
if (val) g_settings.inspector_h = parse_int(val, g_settings.inspector_h);
}
void settings_load_user(void) {
/* Set defaults first — this resets the entire struct, so global
* settings loaded earlier would be lost. To preserve global settings,
* the caller should call settings_load_global() AFTER this function
* if both need to be (re)loaded. The normal startup flow is:
* 1. settings_set_defaults() (via settings_load_user)
* 2. settings_load_global() (from global.db)
* 3. ... login ...
* 4. settings_load_user() (from per-user browser.db, preserves
* global settings already in memory)
*
* But settings_load_user() does NOT reset defaults — it only reads
* per-user keys, leaving global keys untouched. So the correct flow
* is:
* 1. settings_set_defaults() — once
* 2. settings_load_global() — from global.db
* 3. ... login, switch to per-user db ...
* 4. settings_load_user() — from per-user browser.db
*/
const char *val;
val = db_kv_get("restore_session");
@@ -132,44 +230,67 @@ void settings_load(void) {
val = db_kv_get("tab_drag_reorder");
if (val) g_settings.tab_drag_reorder = parse_bool(val, g_settings.tab_drag_reorder);
val = db_kv_get("agent_server_enabled");
if (val) g_settings.agent_server_enabled = parse_bool(val, g_settings.agent_server_enabled);
val = db_kv_get("agent_server_port");
if (val) {
g_settings.agent_server_port = parse_int(val, g_settings.agent_server_port);
if (g_settings.agent_server_port < 1) g_settings.agent_server_port = SETTINGS_AGENT_PORT_DEFAULT;
}
val = db_kv_get("agent_allowed_origins");
if (val) snprintf(g_settings.agent_allowed_origins, sizeof(g_settings.agent_allowed_origins), "%s", val);
val = db_kv_get("agent_login_timeout_ms");
if (val) {
g_settings.agent_login_timeout_ms = parse_int(val, g_settings.agent_login_timeout_ms);
if (g_settings.agent_login_timeout_ms < 0) g_settings.agent_login_timeout_ms = 0;
}
val = db_kv_get("bootstrap_relays");
if (val) snprintf(g_settings.bootstrap_relays, sizeof(g_settings.bootstrap_relays), "%s", val);
val = db_kv_get("search_engine");
if (val) snprintf(g_settings.search_engine, sizeof(g_settings.search_engine), "%s", val);
val = db_kv_get("theme_dark");
if (val) g_settings.theme_dark = parse_bool(val, g_settings.theme_dark);
val = db_kv_get("inspector_x");
if (val) g_settings.inspector_x = parse_int(val, g_settings.inspector_x);
val = db_kv_get("inspector_y");
if (val) g_settings.inspector_y = parse_int(val, g_settings.inspector_y);
val = db_kv_get("inspector_w");
if (val) g_settings.inspector_w = parse_int(val, g_settings.inspector_w);
val = db_kv_get("inspector_h");
if (val) g_settings.inspector_h = parse_int(val, g_settings.inspector_h);
}
void settings_save(void) {
void settings_load(void) {
/* Backward-compatible full load: sets defaults, then loads all
* settings from the currently-open database. Used when a single
* database contains all settings (legacy or test scenarios). */
settings_set_defaults(&g_settings);
settings_load_global();
settings_load_user();
}
void settings_save_global(void) {
/* Save global settings to global.db. This uses db_kv_set_to_file()
* which opens a separate short-lived connection, so it works even
* when the per-user browser.db is the main open database (the normal
* case after login). At startup, when global.db IS the main open
* database, this also works (the separate connection is fine since
* SQLite handles concurrent readers/writers with FULLMUTEX). */
char gpath[512];
global_db_path(gpath, sizeof(gpath));
if (gpath[0] == '\0') {
g_printerr("[settings] Failed to get global.db path for save\n");
return;
}
char buf[32];
db_kv_set_to_file(gpath, "agent_server_enabled",
g_settings.agent_server_enabled ? "true" : "false");
snprintf(buf, sizeof(buf), "%d", g_settings.agent_server_port);
db_kv_set_to_file(gpath, "agent_server_port", buf);
db_kv_set_to_file(gpath, "agent_allowed_origins",
g_settings.agent_allowed_origins);
snprintf(buf, sizeof(buf), "%d", g_settings.agent_login_timeout_ms);
db_kv_set_to_file(gpath, "agent_login_timeout_ms", buf);
db_kv_set_to_file(gpath, "theme_dark",
g_settings.theme_dark ? "true" : "false");
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_x);
db_kv_set_to_file(gpath, "inspector_x", buf);
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_y);
db_kv_set_to_file(gpath, "inspector_y", buf);
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_w);
db_kv_set_to_file(gpath, "inspector_w", buf);
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_h);
db_kv_set_to_file(gpath, "inspector_h", buf);
}
void settings_save_user(void) {
/* Save per-user settings to the currently-open database (should be
* the per-user browser.db). Called when the user changes per-user
* settings on the settings page. */
char buf[32];
db_kv_set("restore_session", g_settings.restore_session ? "true" : "false");
@@ -183,30 +304,21 @@ void settings_save(void) {
db_kv_set("max_tabs", buf);
db_kv_set("tab_drag_reorder", g_settings.tab_drag_reorder ? "true" : "false");
db_kv_set("agent_server_enabled", g_settings.agent_server_enabled ? "true" : "false");
snprintf(buf, sizeof(buf), "%d", g_settings.agent_server_port);
db_kv_set("agent_server_port", buf);
db_kv_set("agent_allowed_origins", g_settings.agent_allowed_origins);
snprintf(buf, sizeof(buf), "%d", g_settings.agent_login_timeout_ms);
db_kv_set("agent_login_timeout_ms", buf);
/* Bootstrap relays are stored as-is (newlines are fine in SQLite). */
db_kv_set("bootstrap_relays", g_settings.bootstrap_relays);
db_kv_set("search_engine", g_settings.search_engine);
db_kv_set("theme_dark", g_settings.theme_dark ? "true" : "false");
}
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_x);
db_kv_set("inspector_x", buf);
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_y);
db_kv_set("inspector_y", buf);
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_w);
db_kv_set("inspector_w", buf);
snprintf(buf, sizeof(buf), "%d", g_settings.inspector_h);
db_kv_set("inspector_h", buf);
void settings_save(void) {
/* Backward-compatible full save: saves all settings to the
* currently-open database. The settings page should call
* settings_save_user() + settings_save_global() instead, so that
* global settings go to global.db and per-user settings go to the
* per-user browser.db. */
settings_save_user();
settings_save_global();
}
const browser_settings_t *settings_get(void) {
+36 -5
View File
@@ -15,7 +15,7 @@
extern "C" {
#endif
#define SETTINGS_NEW_TAB_URL_DEFAULT "https://example.com"
#define SETTINGS_NEW_TAB_URL_DEFAULT ""
#define SETTINGS_NEW_TAB_URL_MAX 512
#define SETTINGS_MAX_TABS_DEFAULT 50
#define SETTINGS_AGENT_PORT_DEFAULT 17777
@@ -52,17 +52,48 @@ typedef struct {
} browser_settings_t;
/*
* Load settings from disk into the global singleton.
* If the file doesn't exist or a key is missing, defaults are used.
* Call once at startup.
* Load all settings from the currently-open database into the global
* singleton. Sets defaults first, then reads all keys.
* Backward-compatible — prefer settings_load_global() + settings_load_user()
* for the split-database flow.
*/
void settings_load(void);
/*
* Save the current global settings to disk.
* Load global settings (agent server, theme, inspector geometry) from
* the currently-open database (should be global.db). Does NOT reset
* defaults — call settings_set_defaults() first (or use the full
* settings_load() if you need defaults reset).
*/
void settings_load_global(void);
/*
* Load per-user settings (tabs, relays, search engine) from the
* currently-open database (should be the per-user browser.db). Does NOT
* reset defaults — only reads per-user keys, leaving global keys in the
* struct untouched.
*/
void settings_load_user(void);
/*
* Save all settings to the currently-open database.
* Backward-compatible — prefer settings_save_global() + settings_save_user()
* for the split-database flow.
*/
void settings_save(void);
/*
* Save global settings to the currently-open database (should be
* global.db). Call when global settings change.
*/
void settings_save_global(void);
/*
* Save per-user settings to the currently-open database (should be the
* per-user browser.db). Call when per-user settings change.
*/
void settings_save_user(void);
/*
* Get a pointer to the global settings singleton.
* Always valid after settings_load().
+2 -2
View File
@@ -11,9 +11,9 @@
#ifndef SOVEREIGN_BROWSER_VERSION_H
#define SOVEREIGN_BROWSER_VERSION_H
#define SB_VERSION "v0.0.19"
#define SB_VERSION "v0.0.26"
#define SB_VERSION_MAJOR 0
#define SB_VERSION_MINOR 0
#define SB_VERSION_PATCH 19
#define SB_VERSION_PATCH 26
#endif /* SOVEREIGN_BROWSER_VERSION_H */