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:
- See FIPS status — daemon state, node npub, TUN interface, peer count, tree state, ownership (managed vs attached), error messages.
- See connected peers — list of peers with their npub, address, transport, and connection state.
- Connect to a peer — add a new peer by npub + address + transport.
- Disconnect a peer — remove a peer connection.
- Restart the FIPS service — stop + start the managed daemon, or re-attach.
- 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:
fips_control_connect()— open a control connection to the socket.fips_control_show_status()— sends{"command":"show_status"}, parses response intofips_status_t(npub, peer_count, tun_state, tun_name, daemon state, tree_state, tun_active).fips_control_show_peers()— sends{"command":"show_peers"}, returns thedatafield as a JSON string (caller frees). This is not currently called anywhere — it exists but is unused.fips_control_connect_peer()— sends{"command":"connect","params":{"npub":...,"address":...,"transport":...}}. Also unused.fips_control_close()— close the fd.
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 globalg_services[NET_SERVICE_FIPS]struct. Thestatus.fipsunion member has:peer_count,node_npub[80],tree_state[32],tun_active,tun_name[16],daemon_state[32],control_socket[512].fips_poll_cb()pollsshow_statusevery 2s (managed) or 5s (attached) and updates the struct. It does not pollshow_peers— only the peer count is tracked.- The service
statefield (service_state_t) tracks the lifecycle:SERVICE_DISABLED,DISCOVERING,ATTACHING,STARTING,BOOTSTRAPPING,READY,STOPPING,EXITED,FAILED. ownershipisOWNERSHIP_NONE,ATTACHED, orMANAGED.error_msgholds the failure reason whenstate == 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:
- HTML page served from an embedded
www/file viaserve_embedded_file(). - CSS served from
www/<name>.css, linked via<link href="sovereign://fips.css">. - JS served from
www/<name>.js, loaded via<script src="sovereign://fips.js">. - JSON data endpoints —
sovereign://fips/statusreturns JSON, fetched by the JS on page load and on refresh. - Action endpoints —
sovereign://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)— readsnet_service_get_status(NET_SERVICE_FIPS)andnet_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 (fromstatus.fips.control_socket), callsfips_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)— parsesnpub,address,transportfrom query string, opens a control connection, callsfips_control_connect_peer(), responds as JSON.handle_fips_disconnect(request, query)— parsesnpub, opens a control connection, callsfips_control_disconnect_peer(), responds as JSON.handle_fips_restart(request)— callsnet_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.
7. Add a link to the FIPS page from the settings page
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)
- Add
handle_fips_status_json()tonostr_bridge.c - Add
sovereign://fips/fips.css/fips.js/fips/statusroutes - Create
www/fips.html/fips.css/fips.jswith status display - Show FIPS state, node npub, TUN, peer count, tree state, ownership
- Show Tor status (read-only)
- Auto-refresh every 5s
- Add link from settings page
Phase 2: Peer management (interactive)
- Add
handle_fips_peers_json()— callfips_control_show_peers() - Add
handle_fips_connect()— callfips_control_connect_peer() - Add
fips_control_disconnect_peer()tofips_control.c - Add
handle_fips_disconnect()— callfips_control_disconnect_peer() - Add peers table + add-peer form + disconnect buttons to the page
- Test: connect to a known FIPS peer, verify it appears in the list,
verify
.fipsURLs to that peer resolve in the browser
Phase 3: Service control
- Add
handle_fips_restart()— callnet_service_restart(NET_SERVICE_FIPS) - Add restart button to the page
- Poll status during restart (state transitions: READY → STOPPING → DISCOVERING → STARTING → READY)
- Show error messages when state is FAILED
Phase 4: Polish
- Copy-to-clipboard for node npub
- QR code for node npub (reuse
sovereign://qr?text=...) - Bootstrap peer suggestions (if FIPS publishes a test mesh peer list)
- Transport auto-detect (try udp, then tcp)
- Address book — save known peers in SQLite for re-connecting after restart
Testing
- Build and start browser
- Navigate to
sovereign://fips - Verify FIPS status shows correctly (state, npub, TUN, peer count)
- Verify Tor status shows correctly
- Verify auto-refresh updates peer count if peers change
- If FIPS is not running:
- Verify status shows "disabled" or "failed" with error message
- Verify peers list is empty with "FIPS not ready" message
- 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
- Disconnect a peer:
- Click the ✕ next to a peer
- Verify peer disappears from the list
- Restart FIPS:
- Click Restart Service
- Verify state transitions through stopping → starting → ready
- Verify peers reconnect (if persistent) or need re-adding
Open Questions
-
FIPS disconnect command name — is it
disconnect,disconnect_peer, orremove_peer? Need to check the FIPS daemon's control protocol docs orfipsctlsource. Theconnectcommand is confirmed (used infips_control_connect_peer), butdisconnectis assumed. -
Peer persistence — when the browser restarts the managed FIPS daemon, do peers persist (from
fips.yamlor 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. -
FIPS daemon control protocol docs — the
show_peersresponse 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.