/* * cli.c — command-line flag parsing for sovereign_browser * * Uses getopt_long to parse flags before gtk_init(). Recognized flags * are stripped from argv (getopt_long reorders argv in place; optind * points at the first positional arg on return). We collect positional * args as startup URLs. * * Login flags build a cJSON params object matching the shape documented * in agent_login.h and call agent_login(), reusing the same code path * as the MCP 'login' tool. */ #define _POSIX_C_SOURCE 200809L /* strdup, getopt_long */ #include "cli.h" #include "version.h" #include "agent_login.h" #include "key_store.h" #include "nostr_core/nostr_core.h" #include #include #include #include /* ── Usage text ────────────────────────────────────────────────────── */ void cli_print_usage(FILE *fp) { fprintf(fp, "sovereign_browser %s — WebKitGTK + C99\n" "\n" "Usage: sovereign_browser [OPTIONS] [URL...]\n" "\n" "Positional URLs are opened in tabs at startup (unless a session is\n" "restored). --url flags are equivalent and may be repeated.\n" "\n" "Browser / Startup:\n" " --url Open URL in a tab (repeatable)\n" " --new-tab-url Override the default new-tab URL\n" " --no-session-restore Skip session restore for this run\n" " --session-restore Force session restore even if disabled\n" " --max-tabs Override max simultaneous tabs\n" " -V, --version Print version and exit\n" " -h, --help Show this help and exit\n" "\n" "Agent Server:\n" " --port Override agent MCP server port\n" " --no-agent Disable the agent server for this run\n" " --agent Force-enable the agent server\n" " --agent-origin Allowed origin (repeatable)\n" "\n" "Login (skips the GTK login dialog):\n" " --no-login Browse without a Nostr identity (normal browser mode)\n" " --login-method Method: generate|local|seed|readonly|nip46|nsigner\n" " --nsec (local) nsec bech32 private key\n" " --privkey (local) 64-char hex private key\n" " --mnemonic (seed) BIP-39 mnemonic, quoted\n" " --account (seed) BIP-44 account index (default 0)\n" " --npub (readonly) npub bech32 public key\n" " --pubkey (readonly) 64-char hex pubkey\n" " --bunker (nip46) bunker:// URL\n" " --nsigner-transport (nsigner) serial|unix|tcp|qrexec\n" " --nsigner-device (nsigner) device path / socket / host:port / qube\n" " --nsigner-service (nsigner) qrexec service name\n" " --nsigner-index (nsigner) key index (default 0)\n" " --login-timeout Override agent login timeout (GTK dialog path)\n" "\n" "Diagnostics:\n" " -v, --verbose Increase log verbosity (repeatable)\n" " -q, --quiet Suppress non-error log output\n" "\n" "Examples:\n" " sovereign_browser https://example.com\n" " sovereign_browser --login-method generate --url https://example.com\n" " sovereign_browser --login-method local --nsec nsec1... --port 18888\n" " sovereign_browser --login-method readonly --npub npub1... --no-agent\n" " sovereign_browser --no-agent --url https://a.com --url https://b.com\n", SB_VERSION); } /* ── getopt_long option table ──────────────────────────────────────── */ enum { OPT_URL = 1000, OPT_NEW_TAB_URL, OPT_NO_SESSION_RESTORE, OPT_SESSION_RESTORE, OPT_MAX_TABS, OPT_VERSION, OPT_PORT, OPT_NO_AGENT, OPT_AGENT, OPT_AGENT_ORIGIN, OPT_LOGIN_METHOD, OPT_NSEC, OPT_PRIVKEY, OPT_MNEMONIC, OPT_ACCOUNT, OPT_NPUB, OPT_PUBKEY, OPT_BUNKER, OPT_NSIGNER_TRANSPORT, OPT_NSIGNER_DEVICE, OPT_NSIGNER_SERVICE, OPT_NSIGNER_INDEX, OPT_NO_LOGIN, OPT_LOGIN_TIMEOUT, OPT_VERBOSE, OPT_QUIET, OPT_HELP, }; static struct option long_opts[] = { /* Browser / startup */ {"url", required_argument, 0, OPT_URL}, {"new-tab-url", required_argument, 0, OPT_NEW_TAB_URL}, {"no-session-restore", no_argument, 0, OPT_NO_SESSION_RESTORE}, {"session-restore", no_argument, 0, OPT_SESSION_RESTORE}, {"max-tabs", required_argument, 0, OPT_MAX_TABS}, {"version", no_argument, 0, OPT_VERSION}, /* Agent server */ {"port", required_argument, 0, OPT_PORT}, {"no-agent", no_argument, 0, OPT_NO_AGENT}, {"agent", no_argument, 0, OPT_AGENT}, {"agent-origin", required_argument, 0, OPT_AGENT_ORIGIN}, /* Login */ {"login-method", required_argument, 0, OPT_LOGIN_METHOD}, {"nsec", required_argument, 0, OPT_NSEC}, {"privkey", required_argument, 0, OPT_PRIVKEY}, {"mnemonic", required_argument, 0, OPT_MNEMONIC}, {"account", required_argument, 0, OPT_ACCOUNT}, {"npub", required_argument, 0, OPT_NPUB}, {"pubkey", required_argument, 0, OPT_PUBKEY}, {"bunker", required_argument, 0, OPT_BUNKER}, {"nsigner-transport", required_argument, 0, OPT_NSIGNER_TRANSPORT}, {"nsigner-device", required_argument, 0, OPT_NSIGNER_DEVICE}, {"nsigner-service", required_argument, 0, OPT_NSIGNER_SERVICE}, {"nsigner-index", required_argument, 0, OPT_NSIGNER_INDEX}, {"no-login", no_argument, 0, OPT_NO_LOGIN}, {"login-timeout", required_argument, 0, OPT_LOGIN_TIMEOUT}, /* Diagnostics */ {"verbose", no_argument, 0, OPT_VERBOSE}, {"quiet", no_argument, 0, OPT_QUIET}, {"help", no_argument, 0, OPT_HELP}, {0, 0, 0, 0} }; static const char *short_opts = "vVhq"; /* ── Helpers ───────────────────────────────────────────────────────── */ static char *xstrdup(const char *s) { if (s == NULL) return NULL; char *p = strdup(s); if (p == NULL) { fprintf(stderr, "[cli] out of memory\n"); exit(EXIT_FAILURE); } return p; } static void add_string(char ***arr, int *count, const char *value) { *arr = g_realloc(*arr, sizeof(char *) * (*count + 1)); (*arr)[*count] = xstrdup(value); (*count)++; } static int valid_login_method(const char *m) { return strcmp(m, "generate") == 0 || strcmp(m, "local") == 0 || strcmp(m, "seed") == 0 || strcmp(m, "readonly") == 0 || strcmp(m, "nip46") == 0 || strcmp(m, "nsigner") == 0; } /* ── cli_args_free ─────────────────────────────────────────────────── */ void cli_args_free(cli_args_t *args) { if (args == NULL) return; if (args->urls) { for (int i = 0; i < args->url_count; i++) g_free(args->urls[i]); g_free(args->urls); } g_free(args->new_tab_url); if (args->agent_origins) { for (int i = 0; i < args->agent_origin_count; i++) g_free(args->agent_origins[i]); g_free(args->agent_origins); } g_free(args->login_method); g_free(args->nsec); g_free(args->privkey); g_free(args->mnemonic); g_free(args->npub); g_free(args->pubkey); g_free(args->bunker); g_free(args->nsigner_transport); g_free(args->nsigner_device); g_free(args->nsigner_service); memset(args, 0, sizeof(*args)); } /* ── cli_parse ─────────────────────────────────────────────────────── */ int cli_parse(int *argc, char ***argv, cli_args_t *out) { memset(out, 0, sizeof(*out)); out->account = -1; out->nsigner_index = -1; /* getopt_long reorders argv so non-options end up at the end. */ opterr = 0; /* we print our own errors */ optind = 1; int c; int long_idx; while ((c = getopt_long(*argc, *argv, short_opts, long_opts, &long_idx)) != -1) { switch (c) { /* Short options */ case 'v': out->verbose++; break; case 'V': out->want_version = TRUE; return 0; case 'h': out->want_help = TRUE; return 0; case 'q': out->quiet = TRUE; break; case '?': if (optopt) { fprintf(stderr, "[cli] unknown short option '-%c'\n", optopt); } else { fprintf(stderr, "[cli] unknown long option near '%s'\n", (*argv)[optind - 1] ? (*argv)[optind - 1] : "?"); } cli_print_usage(stderr); return -1; /* Browser / startup */ case OPT_URL: add_string(&out->urls, &out->url_count, optarg); break; case OPT_NEW_TAB_URL: g_free(out->new_tab_url); out->new_tab_url = xstrdup(optarg); break; case OPT_NO_SESSION_RESTORE: out->session_restore = CLI_TRISTATE_FALSE; break; case OPT_SESSION_RESTORE: out->session_restore = CLI_TRISTATE_TRUE; break; case OPT_MAX_TABS: out->max_tabs = atoi(optarg); if (out->max_tabs < 1) { fprintf(stderr, "[cli] --max-tabs must be >= 1\n"); return -1; } break; case OPT_VERSION: out->want_version = TRUE; return 0; /* Agent server */ case OPT_PORT: out->agent_port = atoi(optarg); if (out->agent_port < 1 || out->agent_port > 65535) { fprintf(stderr, "[cli] --port must be 1-65535\n"); return -1; } break; case OPT_NO_AGENT: out->agent_enabled = CLI_TRISTATE_FALSE; break; case OPT_AGENT: out->agent_enabled = CLI_TRISTATE_TRUE; break; case OPT_AGENT_ORIGIN: add_string(&out->agent_origins, &out->agent_origin_count, optarg); break; /* Login */ case OPT_LOGIN_METHOD: if (!valid_login_method(optarg)) { fprintf(stderr, "[cli] invalid --login-method '%s'\n", optarg); fprintf(stderr, "[cli] use: generate|local|seed|readonly|nip46|nsigner\n"); return -1; } g_free(out->login_method); out->login_method = xstrdup(optarg); break; case OPT_NSEC: g_free(out->nsec); out->nsec = xstrdup(optarg); break; case OPT_PRIVKEY: g_free(out->privkey); out->privkey = xstrdup(optarg); break; case OPT_MNEMONIC: g_free(out->mnemonic); out->mnemonic = xstrdup(optarg); break; case OPT_ACCOUNT: out->account = atoi(optarg); if (out->account < 0) { fprintf(stderr, "[cli] --account must be >= 0\n"); return -1; } break; case OPT_NPUB: g_free(out->npub); out->npub = xstrdup(optarg); break; case OPT_PUBKEY: g_free(out->pubkey); out->pubkey = xstrdup(optarg); break; case OPT_BUNKER: g_free(out->bunker); out->bunker = xstrdup(optarg); break; case OPT_NSIGNER_TRANSPORT: g_free(out->nsigner_transport); out->nsigner_transport = xstrdup(optarg); break; case OPT_NSIGNER_DEVICE: g_free(out->nsigner_device); out->nsigner_device = xstrdup(optarg); break; case OPT_NSIGNER_SERVICE: g_free(out->nsigner_service); out->nsigner_service = xstrdup(optarg); break; case OPT_NSIGNER_INDEX: out->nsigner_index = atoi(optarg); if (out->nsigner_index < 0) { fprintf(stderr, "[cli] --nsigner-index must be >= 0\n"); return -1; } break; case OPT_NO_LOGIN: out->no_login = TRUE; break; case OPT_LOGIN_TIMEOUT: out->login_timeout_ms = atoi(optarg); if (out->login_timeout_ms < 0) { fprintf(stderr, "[cli] --login-timeout must be >= 0\n"); return -1; } break; /* Diagnostics */ case OPT_VERBOSE: out->verbose++; break; case OPT_QUIET: out->quiet = TRUE; break; case OPT_HELP: out->want_help = TRUE; return 0; default: fprintf(stderr, "[cli] unhandled option\n"); return -1; } } /* Validate login-method-specific args. */ if (out->login_method) { const char *m = out->login_method; if (strcmp(m, "local") == 0) { if (!out->nsec && !out->privkey) { fprintf(stderr, "[cli] --login-method local requires --nsec or --privkey\n"); return -1; } } else if (strcmp(m, "seed") == 0) { if (!out->mnemonic) { fprintf(stderr, "[cli] --login-method seed requires --mnemonic\n"); return -1; } } else if (strcmp(m, "readonly") == 0) { if (!out->npub && !out->pubkey) { fprintf(stderr, "[cli] --login-method readonly requires --npub or --pubkey\n"); return -1; } } else if (strcmp(m, "nip46") == 0) { if (!out->bunker) { fprintf(stderr, "[cli] --login-method nip46 requires --bunker\n"); return -1; } } else if (strcmp(m, "nsigner") == 0) { if (!out->nsigner_transport || !out->nsigner_device) { fprintf(stderr, "[cli] --login-method nsigner requires --nsigner-transport and --nsigner-device\n"); return -1; } } /* "generate" needs no extra args. */ } /* Collect positional args (after getopt_long reordering) as URLs. */ for (int i = optind; i < *argc; i++) { add_string(&out->urls, &out->url_count, (*argv)[i]); } /* Repack argv: keep argv[0], then positional args only. gtk_init() * will see a clean vector with no unknown options. */ int new_argc = 1; /* argv[0] */ for (int i = optind; i < *argc; i++) { (*argv)[new_argc++] = (*argv)[i]; } (*argv)[new_argc] = NULL; *argc = new_argc; return 0; } /* ── cli_login ─────────────────────────────────────────────────────── */ int cli_login(const cli_args_t *args) { if (args->login_method == NULL) { fprintf(stderr, "[cli] cli_login called with no --login-method\n"); return -1; } /* Build the cJSON params object matching agent_login.h's contract. */ cJSON *params = cJSON_CreateObject(); cJSON_AddStringToObject(params, "method", args->login_method); if (args->nsec) cJSON_AddStringToObject(params, "nsec", args->nsec); if (args->privkey) cJSON_AddStringToObject(params, "privkey_hex", args->privkey); if (args->mnemonic) cJSON_AddStringToObject(params, "mnemonic", args->mnemonic); if (args->account >= 0) cJSON_AddNumberToObject(params, "account", args->account); if (args->npub) cJSON_AddStringToObject(params, "npub", args->npub); if (args->pubkey) cJSON_AddStringToObject(params, "pubkey_hex", args->pubkey); if (args->bunker) cJSON_AddStringToObject(params, "bunker_url", args->bunker); if (args->nsigner_transport) cJSON_AddStringToObject(params, "transport", args->nsigner_transport); if (args->nsigner_device) cJSON_AddStringToObject(params, "device", args->nsigner_device); if (args->nsigner_service) cJSON_AddStringToObject(params, "service", args->nsigner_service); if (args->nsigner_index >= 0) cJSON_AddNumberToObject(params, "index", args->nsigner_index); /* agent_login() calls app_set_signer() on success, which sets the * same global state the GTK dialog would. It also handles * nostr_init() internally. */ cJSON *result = agent_login(params); cJSON_Delete(params); if (result == NULL) { fprintf(stderr, "[cli] login returned NULL\n"); return -1; } cJSON *success = cJSON_GetObjectItem(result, "success"); if (!success || !cJSON_IsTrue(success)) { cJSON *err = cJSON_GetObjectItem(result, "error"); const char *code = err ? cJSON_GetStringValue(cJSON_GetObjectItem(err, "code")) : "?"; const char *msg = err ? cJSON_GetStringValue(cJSON_GetObjectItem(err, "message")) : "unknown"; fprintf(stderr, "[cli] login failed: %s: %s\n", code ? code : "?", msg ? msg : "?"); cJSON_Delete(result); return -1; } /* Login succeeded. Print the pubkey for visibility. */ cJSON *data = cJSON_GetObjectItem(result, "data"); if (data) { const char *pubkey = cJSON_GetStringValue(cJSON_GetObjectItem(data, "pubkey")); const char *method = cJSON_GetStringValue(cJSON_GetObjectItem(data, "method")); g_print("[cli] Logged in: method=%s pubkey=%s\n", method ? method : "?", pubkey ? pubkey : "?"); } cJSON_Delete(result); /* Private keys are never persisted to disk — they live only in RAM * for the duration of the session. The identity is held in the * global app_state_t (main.c) and is gone when the browser quits. */ return 0; }