v0.0.20 - Per-user profile directory system: each Nostr identity gets its own browser.db (events, history, session, bookmarks) in ~/.sovereign_browser/profiles/<pubkey>/, global settings in global.db, identity persistence per-profile

This commit is contained in:
Laan Tungir
2026-07-13 07:02:54 -04:00
parent 02a92f7a26
commit a4b4c52a91
13 changed files with 1058 additions and 91 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.20
+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
+134 -13
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);
}
@@ -428,6 +447,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 +545,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 +695,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);
+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 */
+166 -54
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 ─────────────────────────────────────────────── */
@@ -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) {
+35 -4
View File
@@ -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.20"
#define SB_VERSION_MAJOR 0
#define SB_VERSION_MINOR 0
#define SB_VERSION_PATCH 19
#define SB_VERSION_PATCH 20
#endif /* SOVEREIGN_BROWSER_VERSION_H */