Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions docs/reticulum-sidecar-ipc.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ Electron main validates proxy paths: must start with `/api/v1/` (no `..` segment

### Identity

| Method | Path | Body / notes | Response |
| ------ | --------------------------------- | -------------------------------------------------- | --------------------------------------------------------- |
| GET | `/api/v1/identity/status` | | `{ configured, identity_hash, lxmf_hash, display_name? }` |
| POST | `/api/v1/identity/generate` | `{ display_name?, replace? }` | `{ ok, mnemonic?, identity_hash, lxmf_hash }` |
| POST | `/api/v1/identity/import` | `{ mnemonic, display_name?, replace? }` | `{ ok, identity_hash, lxmf_hash }` |
| POST | `/api/v1/identity/import-backup` | `{ backup, passphrase?, display_name?, replace? }` | `{ ok, identity_hash, lxmf_hash, metadata_only? }` |
| POST | `/api/v1/identity/import-private` | `{ private_key, display_name?, replace? }` | `{ ok, identity_hash, lxmf_hash }` |
| POST | `/api/v1/identity/export` | `{ passphrase }` | `{ ok, backup? }` |
| POST | `/api/v1/identity/display-name` | `{ display_name }` | `{ ok }` |
| Method | Path | Body / notes | Response |
| ------ | --------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------- |
| GET | `/api/v1/identity/status` | | `{ configured, identity_hash, lxmf_hash, display_name?, public_key? }` |
| POST | `/api/v1/identity/register-known` | `{ destination_hash, public_key }` | `{ ok }` (registers 64-byte pubkey for Direct LXMF / Columba QR) |
| POST | `/api/v1/identity/generate` | `{ display_name?, replace? }` | `{ ok, mnemonic?, identity_hash, lxmf_hash }` |
| POST | `/api/v1/identity/import` | `{ mnemonic, display_name?, replace? }` | `{ ok, identity_hash, lxmf_hash }` |
| POST | `/api/v1/identity/import-backup` | `{ backup, passphrase?, display_name?, replace? }` | `{ ok, identity_hash, lxmf_hash, metadata_only? }` |
| POST | `/api/v1/identity/import-private` | `{ private_key, display_name?, replace? }` | `{ ok, identity_hash, lxmf_hash }` |
| POST | `/api/v1/identity/export` | `{ passphrase }` | `{ ok, backup? }` |
| POST | `/api/v1/identity/display-name` | `{ display_name }` | `{ ok }` |

### Interfaces

Expand Down Expand Up @@ -97,14 +98,14 @@ Routing bias between **RF** (LoRa / RNode) and **network** (TCP/UDP/I2P/gateway/

### Peers, topology, and propagation

| Method | Path | Body / notes | Response |
| ------ | ---------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GET | `/api/v1/peers` | `?refresh=1` optional | `{ peers: [] }` — live path table when `rns-stack` enabled; without `refresh=1` may serve a short-TTL maintenance cache; `refresh=1`/`true` forces live `GetPathTable` (manual Refresh). `display_name` overlayed from contacts/Nomad/announce label cache |
| POST | `/api/v1/peers/{hash}/path` | | `{ ok }` — emits `peers_updated` WS on success |
| POST | `/api/v1/peers/{hash}/probe` | | `{ ok, hops? }` live; `{ ok, mode, hash }` stub — emits `peers_updated` on success |
| POST | `/api/v1/ping` | `{ destination_hash }` | `{ ok, rtt_ms? }` |
| GET | `/api/v1/topology` | | `{ nodes, edges, total?, shown?, truncated? }` — `via_hash` is the immediate RNS next hop (transport id); sidecar infers `self → relay` when needed |
| GET | `/api/v1/rmap/discovered` | | `{ discovered: RmapDiscoveredWireRow[] }` — local RMAP v4 heard interfaces (7-day TTL eviction in rsReticulum DiscoveryStore) |
| Method | Path | Body / notes | Response |
| ------ | ---------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GET | `/api/v1/peers` | `?refresh=1` optional | `{ peers: [] }` — live path table when `rns-stack` enabled; without `refresh=1` may serve a short-TTL maintenance cache; `refresh=1`/`true` forces live `GetPathTable` (manual Refresh). `display_name` overlayed from contacts/Nomad/announce label cache; optional `public_key` (128 hex) when known from announces |
| POST | `/api/v1/peers/{hash}/path` | | `{ ok }` — emits `peers_updated` WS on success |
| POST | `/api/v1/peers/{hash}/probe` | | `{ ok, hops? }` live; `{ ok, mode, hash }` stub — emits `peers_updated` on success |
| POST | `/api/v1/ping` | `{ destination_hash }` | `{ ok, rtt_ms? }` |
| GET | `/api/v1/topology` | | `{ nodes, edges, total?, shown?, truncated? }` — `via_hash` is the immediate RNS next hop (transport id); sidecar infers `self → relay` when needed |
| GET | `/api/v1/rmap/discovered` | | `{ discovered: RmapDiscoveredWireRow[] }` — local RMAP v4 heard interfaces (7-day TTL eviction in rsReticulum DiscoveryStore) |

**`RmapDiscoveredWireRow` fields** (see `src/shared/reticulum-types.ts`): `discovery_hash`, `transport_id`, `discovery_name`, `interface_type`, `latitude`, `longitude`, `height`, `transport_enabled`, `reachable_on`, LoRa RF fields (`frequency`, `bandwidth`, `spreading_factor`, …), `hops`, `stamp_value`, `discovered`, `last_heard`, `heard_count`, `status` (`available`/`stale`/`unknown`), `has_coordinates`. Renderer caps at 2,000 newest rows with client-side TTL eviction.

Expand Down
2 changes: 1 addition & 1 deletion docs/reticulum.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ When multiple enabled local RNode interfaces are connected, the interface list s

- **Identity:** generate BIP-39 recovery phrase, import **private key** (paste or file picker via `reticulum:showIdentityImportDialog`), import **backup JSON**, export with passphrase, display name; **replace identity** confirm when keys already exist (`replace: true` on generate/import)
- **Identity slots:** Network panel lists local slots (`GET /api/v1/identities`), create / switch / delete (`POST /api/v1/identities`, `/switch`, `/delete`). Create/switch are serialized and commit the active pointer only after the working key is applied; the sidecar restarts after a successful change. Soft cap **16** slots; display names are sanitized (control chars rejected, max 128 chars).
- **Identity / contact QR:** share via `QrCodeImage` (`lxm://identity/…`, `lxm://contact/…`); ingest via paste/file/camera (`QrIngestControl`). OS deep links use the registered **`lxm://`** scheme (`electron-builder.yml`); external contact imports require confirmation (`MeshClientDeepLinkHost`). Encrypted LXMF paper messages are not supported yet.
- **Identity / contact QR:** share via `QrCodeImage` — Columba-compatible **`lxma://<lxmf_hash>:<public_key>`** when the sidecar reports a public key (identity Network QR and peer detail when known); otherwise mesh-client **`lxm://identity/…`** / **`lxm://contact/…`**. Ingest via paste/file/camera (`QrIngestControl`) including `lxma://` (registers pubkey + saved contact). OS deep links use the registered **`lxm://`** scheme (`electron-builder.yml`); `lxma://` / `meshcore://` are handled when opened/pasted (not OS-registered). External contact imports require confirmation (`MeshClientDeepLinkHost`). Encrypted LXMF paper messages are not supported yet.
- **Peer fingerprint verification:** Peer detail can mark a contact verified (pins `verified_identity_hash` + `verified_at` in SQLite via `db:setReticulumDestinationVerified`) and warns on mismatch when the live announce hash drifts.
- **Header self label:** when configured, the app header shows your Network **display name** (`reticulumSelfNodeLabel.ts`) — not a hash-prefix stub; omit the `Node:` label when no real name is set
- **Identity vault:** optional passcode (minimum 8 characters) to encrypt secrets in the main process; unlock is rate-limited
Expand Down
26 changes: 24 additions & 2 deletions reticulum-sidecar/src/api/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,34 @@ pub struct DisplayNameBody {

pub async fn identity_status(State(stack): State<Arc<StackHandle>>) -> Json<serde_json::Value> {
let id = stack.identity_status().await;
Json(serde_json::json!({
let public_key = stack.identity_public_key_hex().await;
let mut body = serde_json::json!({
"configured": id.configured,
"identity_hash": id.identity_hash,
"lxmf_hash": id.lxmf_hash,
"display_name": id.display_name,
}))
});
if let Some(pk) = public_key {
body["public_key"] = serde_json::Value::String(pk);
}
Json(body)
}

#[derive(Deserialize)]
pub struct RegisterKnownBody {
pub destination_hash: String,
pub public_key: String,
}

/// Register a peer LXMF destination public key (Columba `lxma://` import).
pub async fn identity_register_known(
State(stack): State<Arc<StackHandle>>,
Json(body): Json<RegisterKnownBody>,
) -> Json<serde_json::Value> {
match stack.register_known_identity(&body.destination_hash, &body.public_key) {
Ok(()) => Json(serde_json::json!({ "ok": true })),
Err(e) => Json(serde_json::json!({ "ok": false, "error": e })),
}
}

/// Generate a new identity. The response includes the mnemonic **once** so the
Expand Down
4 changes: 4 additions & 0 deletions reticulum-sidecar/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ pub fn router(stack: Arc<StackHandle>) -> Router {
.route("/api/v1/status", get(status::status))
.route("/api/v1/app/info", get(status::app_info))
.route("/api/v1/identity/status", get(identity::identity_status))
.route(
"/api/v1/identity/register-known",
post(identity::identity_register_known),
)
.route(
"/api/v1/identity/generate",
post(identity::identity_generate),
Expand Down
13 changes: 13 additions & 0 deletions reticulum-sidecar/src/stack/identity_apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,19 @@ mod tests {
assert_eq!(state.identity.lxmf_hash, expected.lxmf_hash);
}

#[test]
fn applied_identity_exposes_64_byte_public_key() {
let (_root, config_dir, storage_dir) = temp_dirs();
let (identity, _) = generate_identity_with_mnemonic().unwrap();
let expected = hex::encode(identity.get_public_key());
assert_eq!(expected.len(), 128);
let mut state = PersistedState::default_empty();
apply_unified_identity(&mut state, &config_dir, &storage_dir, &identity, None, None)
.unwrap();
let loaded = load_identity_from_file(&config_dir).unwrap();
assert_eq!(hex::encode(loaded.get_public_key()), expected);
}

#[test]
fn reconcile_fixes_stale_json() {
let (_root, config_dir, storage_dir) = temp_dirs();
Expand Down
Loading