Files
sovereign_browser/plans/fips-management-page.md

19 KiB

FIPS Management Page — sovereign://fips

Goal

A dedicated sovereign://fips page for managing the FIPS mesh daemon from inside the browser. This is not part of the settings page — it's a separate status + management page, like a network control panel.

The page lets the user:

  1. See FIPS status — daemon state, node npub, TUN interface, peer count, tree state, ownership (managed vs attached), error messages.
  2. See connected peers — list of peers with their npub, address, transport, and connection state.
  3. Connect to a peer — add a new peer by npub + address + transport.
  4. Disconnect a peer — remove a peer connection.
  5. Restart the FIPS service — stop + start the managed daemon, or re-attach.
  6. See Tor status (bonus) — since Tor and FIPS are the two network services, showing both on one "Network" page makes sense. Tor is read-only status (bootstrap progress, SOCKS endpoint); FIPS is interactive management.

This makes .fips addresses actually usable: the user can add peers from the browser UI instead of dropping to fipsctl on the command line.


Current State

FIPS control API (src/fips_control.c / src/fips_control.h)

The FIPS daemon exposes a JSON-line control protocol over a Unix socket. The browser already has a client:

Missing from the client: a disconnect peer command. The FIPS daemon likely supports {"command":"disconnect","params":{"npub":...}} (this needs verification against the FIPS daemon's control protocol — see fipsctl source or docs/control-api.md in the FIPS repo). We'll add a fips_control_disconnect_peer() function.

Net services layer (src/net_services.c / src/net_services.h)

  • net_service_get_status(NET_SERVICE_FIPS) returns a pointer to the global g_services[NET_SERVICE_FIPS] struct. The status.fips union member has: peer_count, node_npub[80], tree_state[32], tun_active, tun_name[16], daemon_state[32], control_socket[512].
  • fips_poll_cb() polls show_status every 2s (managed) or 5s (attached) and updates the struct. It does not poll show_peers — only the peer count is tracked.
  • The service state field (service_state_t) tracks the lifecycle: SERVICE_DISABLED, DISCOVERING, ATTACHING, STARTING, BOOTSTRAPPING, READY, STOPPING, EXITED, FAILED.
  • ownership is OWNERSHIP_NONE, ATTACHED, or MANAGED.
  • error_msg holds the failure reason when state == SERVICE_FAILED.
  • net_service_restart(NET_SERVICE_FIPS) exists — it disables then re-enables the service.

sovereign:// page pattern (src/nostr_bridge.c)

The existing pages (settings, profile, bookmarks, agents) follow this pattern:

  1. HTML page served from an embedded www/ file via serve_embedded_file().
  2. CSS served from www/<name>.css, linked via <link href="sovereign://fips.css">.
  3. JS served from www/<name>.js, loaded via <script src="sovereign://fips.js">.
  4. JSON data endpointssovereign://fips/status returns JSON, fetched by the JS on page load and on refresh.
  5. Action endpointssovereign://fips/connect?npub=...&address=...&transport=... performs an action and returns JSON.

The JS uses XMLHttpRequest (not fetch()) because WebKit's custom scheme handler doesn't support fetch() reliably for sovereign:// URLs — see the comment at nostr_bridge.c:1902. A sovereignGet() helper wraps XHR and returns a Promise.

Routing is in the main bridge_handle_request() function (nostr_bridge.c:3238) — a series of strcmp/strncmp checks on the URI, calling the appropriate handler.


Design

Page layout

┌─────────────────────────────────────────────────────────────┐
│  FIPS Mesh Network                              [Refresh]   │
├─────────────────────────────────────────────────────────────┤
│  ┌─ Status ──────────────────────────────────────────────┐  │
│  │  State:      Ready  ●                                  │  │
│  │  Node:       npub1abc...xyz                           │  │
│  │  TUN:        fips0 (active)                           │  │
│  │  Peers:      3 connected                               │  │
│  │  Tree:       healthy                                   │  │
│  │  Ownership:  managed (spawned by browser)              │  │
│  │  Control:    ~/.sovereign_browser/fips/control.sock    │  │
│  │  [Restart Service]                                     │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                             │
│  ┌─ Peers ───────────────────────────────────────────────┐  │
│  │  npub1def...  203.0.113.5:4242   udp   connected  [✕] │  │
│  │  npub1ghi...  198.51.100.10:4242 tcp   connected  [✕] │  │
│  │  npub1jkl...  fd00::1:4242        udp   connecting [✕] │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                             │
│  ┌─ Add Peer ────────────────────────────────────────────┐  │
│  │  npub:     [npub1...                                  ] │  │
│  │  address:  [host:port                                 ] │  │
│  │  transport: [udp ▼]                                    │  │
│  │  [Connect]                                             │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                             │
│  ┌─ Tor Status (read-only) ──────────────────────────────┐  │
│  │  State:      Ready  ●                                  │  │
│  │  Bootstrap:  100%                                      │  │
│  │  SOCKS:      socks5://127.0.0.1:9050                   │  │
│  │  Ownership:  attached (system Tor)                     │  │
│  └──────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

Routing

URI Handler Returns
sovereign://fips serve www/fips.html HTML page
sovereign://fips.css serve www/fips.css CSS
sovereign://fips.js serve www/fips.js JS
sovereign://fips/status handle_fips_status_json JSON: FIPS + Tor status
sovereign://fips/peers handle_fips_peers_json JSON: peer list from show_peers
sovereign://fips/connect?npub=...&address=...&transport=... handle_fips_connect JSON: connect result
sovereign://fips/disconnect?npub=... handle_fips_disconnect JSON: disconnect result
sovereign://fips/restart handle_fips_restart JSON: restart result

JSON endpoints

sovereign://fips/status

{
  "fips": {
    "state": "ready",
    "enabled": true,
    "ownership": "managed",
    "node_npub": "npub1abc...",
    "peer_count": 3,
    "tun_active": true,
    "tun_name": "fips0",
    "tree_state": "healthy",
    "daemon_state": "running",
    "control_socket": "~/.sovereign_browser/fips/control.sock",
    "error": null
  },
  "tor": {
    "state": "ready",
    "enabled": true,
    "ownership": "attached",
    "bootstrap_progress": 100,
    "socks_endpoint": "socks5://127.0.0.1:9050",
    "circuit_info": "",
    "error": null
  }
}

When a service is DISABLED or FAILED, the fields are populated as best as possible and error holds the failure message.

sovereign://fips/peers

Returns the raw show_peers data from the FIPS daemon (passed through as-is, since the format is defined by FIPS):

{
  "peers": [
    {"npub": "npub1def...", "address": "203.0.113.5:4242", "transport": "udp", "state": "connected"},
    {"npub": "npub1ghi...", "address": "198.51.100.10:4242", "transport": "tcp", "state": "connected"}
  ]
}

If FIPS is not ready, returns {"peers": [], "error": "FIPS not ready"}.

sovereign://fips/connect

Query params: npub, address, transport (optional, defaults to udp).

Returns {"status": "ok"} or {"error": "..."}.

sovereign://fips/disconnect

Query params: npub.

Returns {"status": "ok"} or {"error": "..."}.

sovereign://fips/restart

No params. Calls net_service_restart(NET_SERVICE_FIPS). Returns {"status": "ok"} (the restart is async — the JS polls sovereign://fips/status to see when it's ready again).


Changes Required

1. Add fips_control_disconnect_peer() to src/fips_control.c / .h

int fips_control_disconnect_peer(int fd, const char *npub,
                                 char *error, size_t error_size);

Sends {"command":"disconnect","params":{"npub":"..."}}. Verify the exact command name against the FIPS daemon's control protocol — check fipsctl source or FIPS docs/control-api.md. If the command is disconnect_peer or remove_peer, adjust accordingly.

2. Add FIPS route handlers to src/nostr_bridge.c

New functions (following the existing handler pattern):

  • handle_fips_status_json(request) — reads net_service_get_status(NET_SERVICE_FIPS) and net_service_get_status(NET_SERVICE_TOR), builds a cJSON object with the fields above, responds as JSON.
  • handle_fips_peers_json(request) — opens a control connection to the FIPS socket (from status.fips.control_socket), calls fips_control_show_peers(), wraps the result in {"peers": [...]}, responds as JSON. If FIPS is not ready, returns empty peers + error.
  • handle_fips_connect(request, query) — parses npub, address, transport from query string, opens a control connection, calls fips_control_connect_peer(), responds as JSON.
  • handle_fips_disconnect(request, query) — parses npub, opens a control connection, calls fips_control_disconnect_peer(), responds as JSON.
  • handle_fips_restart(request) — calls net_service_restart(NET_SERVICE_FIPS), responds as JSON.

Control connection management: Each action handler opens a fresh control connection to the FIPS socket, sends the command, closes the connection. This is simpler than reusing the net_service_t's control_fd (which is owned by the poll callback and may be in use). The fips_control_connect() call is fast (Unix socket, 500ms timeout).

3. Add routing to bridge_handle_request() in src/nostr_bridge.c

Add a FIPS route block (before the sovereign://nostr/ catch-all at line 3606), following the same pattern as the settings/bookmarks/agents blocks:

/* Route sovereign://fips — FIPS mesh management page. */
if (strcmp(uri, "sovereign://fips") == 0 ||
    strncmp(uri, "sovereign://fips?", 18) == 0) {
    serve_embedded_file(request, "fips.html");
    return;
}
if (strcmp(uri, "sovereign://fips.css") == 0 ||
    strncmp(uri, "sovereign://fips.css?", 21) == 0) {
    serve_embedded_file(request, "fips.css");
    return;
}
if (strcmp(uri, "sovereign://fips.js") == 0 ||
    strncmp(uri, "sovereign://fips.js?", 20) == 0) {
    serve_embedded_file(request, "fips.js");
    return;
}
if (strcmp(uri, "sovereign://fips/status") == 0 ||
    strncmp(uri, "sovereign://fips/status?", 25) == 0) {
    handle_fips_status_json(request);
    return;
}
if (strcmp(uri, "sovereign://fips/peers") == 0 ||
    strncmp(uri, "sovereign://fips/peers?", 24) == 0) {
    handle_fips_peers_json(request);
    return;
}
if (strncmp(uri, "sovereign://fips/connect?", 24) == 0) {
    handle_fips_connect(request, uri + 24);
    return;
}
if (strncmp(uri, "sovereign://fips/disconnect?", 27) == 0) {
    handle_fips_disconnect(request, uri + 27);
    return;
}
if (strcmp(uri, "sovereign://fips/restart") == 0 ||
    strncmp(uri, "sovereign://fips/restart?", 26) == 0) {
    handle_fips_restart(request);
    return;
}

4. Create www/fips.html

The HTML page. Links fips.css and fips.js. Structure:

  • Status section (populated by JS from sovereign://fips/status)
  • Peers table (populated by JS from sovereign://fips/peers)
  • Add peer form (npub, address, transport dropdown, Connect button)
  • Tor status section (read-only, from the same status JSON)
  • Refresh button (re-fetches status + peers)

Follow the style of www/settings.html / www/bookmarks.html — use sovereign-base.css for theme variables, monospace font, dark theme.

5. Create www/fips.css

Page-specific styles. Import the base theme:

@import "sovereign://sovereign-base.css";

Style the status cards, peers table, add-peer form. Match the existing sovereign:// page aesthetic.

6. Create www/fips.js

The page logic:

/* On load: fetch status + peers, populate DOM.
 * Refresh button: re-fetch.
 * Connect form: POST to sovereign://fips/connect, then refresh peers.
 * Disconnect button: GET sovereign://fips/disconnect?npub=..., then refresh.
 * Restart button: GET sovereign://fips/restart, then poll status.
 * Auto-refresh: poll sovereign://fips/status every 5s (like the poll cb).
 */

Use the sovereignGet() helper (XHR wrapper) that the other pages use. Do NOT use fetch() — it doesn't work with sovereign:// in WebKit.

In www/settings.html (or www/settings.js), add a link: <a href="sovereign://fips">FIPS Mesh Network</a> in a "Network" section. This makes the page discoverable.

8. No Makefile changes needed

The www/ files are auto-embedded by embed_web_files.sh (it globs all *.html, *.css, *.js in www/). Adding www/fips.* files is sufficient — no Makefile edit required.


Files

www/
├── fips.html          # FIPS management page (new)
├── fips.css           # Page styles (new)
├── fips.js            # Page logic (new)
src/
├── nostr_bridge.c     # Add FIPS route handlers + routing
├── fips_control.c     # Add fips_control_disconnect_peer()
├── fips_control.h     # Add disconnect_peer() declaration

Implementation Phases

Phase 1: Status page (read-only)

  1. Add handle_fips_status_json() to nostr_bridge.c
  2. Add sovereign://fips / fips.css / fips.js / fips/status routes
  3. Create www/fips.html / fips.css / fips.js with status display
  4. Show FIPS state, node npub, TUN, peer count, tree state, ownership
  5. Show Tor status (read-only)
  6. Auto-refresh every 5s
  7. Add link from settings page

Phase 2: Peer management (interactive)

  1. Add handle_fips_peers_json() — call fips_control_show_peers()
  2. Add handle_fips_connect() — call fips_control_connect_peer()
  3. Add fips_control_disconnect_peer() to fips_control.c
  4. Add handle_fips_disconnect() — call fips_control_disconnect_peer()
  5. Add peers table + add-peer form + disconnect buttons to the page
  6. Test: connect to a known FIPS peer, verify it appears in the list, verify .fips URLs to that peer resolve in the browser

Phase 3: Service control

  1. Add handle_fips_restart() — call net_service_restart(NET_SERVICE_FIPS)
  2. Add restart button to the page
  3. Poll status during restart (state transitions: READY → STOPPING → DISCOVERING → STARTING → READY)
  4. Show error messages when state is FAILED

Phase 4: Polish

  1. Copy-to-clipboard for node npub
  2. QR code for node npub (reuse sovereign://qr?text=...)
  3. Bootstrap peer suggestions (if FIPS publishes a test mesh peer list)
  4. Transport auto-detect (try udp, then tcp)
  5. Address book — save known peers in SQLite for re-connecting after restart

Testing

  1. Build and start browser
  2. Navigate to sovereign://fips
  3. Verify FIPS status shows correctly (state, npub, TUN, peer count)
  4. Verify Tor status shows correctly
  5. Verify auto-refresh updates peer count if peers change
  6. If FIPS is not running:
    • Verify status shows "disabled" or "failed" with error message
    • Verify peers list is empty with "FIPS not ready" message
  7. Add a peer:
    • Enter a known FIPS peer's npub + address + transport
    • Click Connect
    • Verify peer appears in the peers list
    • Verify http://<peer-npub>.fips/ loads in a tab
  8. Disconnect a peer:
    • Click the ✕ next to a peer
    • Verify peer disappears from the list
  9. Restart FIPS:
    • Click Restart Service
    • Verify state transitions through stopping → starting → ready
    • Verify peers reconnect (if persistent) or need re-adding

Open Questions

  1. FIPS disconnect command name — is it disconnect, disconnect_peer, or remove_peer? Need to check the FIPS daemon's control protocol docs or fipsctl source. The connect command is confirmed (used in fips_control_connect_peer), but disconnect is assumed.

  2. Peer persistence — when the browser restarts the managed FIPS daemon, do peers persist (from fips.yaml or the daemon's state), or does the user need to re-add them every time? If non-persistent, Phase 4's "address book" (saving peers in SQLite) becomes more important.

  3. FIPS daemon control protocol docs — the show_peers response format is passed through as-is. We should verify the field names (npub, address, transport, state) match what the JS expects. If the format differs, the JS can adapt, but it's better to know upfront.