Skip to main content

qualia_client_core/api/
connect.rs

1//! Connection flow: magic link → verify → SocialWebNet peer
2
3#![allow(non_snake_case)]
4
5use super::*;
6
7#[cfg(not(target_arch = "wasm32"))]
8fn resolve_front_door_did(front_door_did: &str) -> Result<String, String> {
9    if !front_door_did.is_empty() {
10        return Ok(front_door_did.to_string());
11    }
12    crate::domains::list_domains()
13        .first()
14        .map(|d| d.front_door_did.clone())
15        .ok_or_else(|| "no domain yet — create one in Domains & Mail first".to_string())
16}
17
18#[cfg(not(target_arch = "wasm32"))]
19fn build_signed_identifier(
20    front_door_did: String,
21    relation_type: String,
22    domain: &str,
23) -> Result<crate::connection_identifier::ConnectionIdentifier, String> {
24    let id = crate::node_identity::NodeIdentity::load_or_create()?;
25    let fdd = resolve_front_door_did(&front_door_did)?;
26    let rendezvous = if domain.is_empty() {
27        vec![]
28    } else {
29        vec![crate::connection_identifier::RendezvousHint {
30            kind: "domain".into(),
31            value: domain.to_string(),
32        }]
33    };
34    let now = mail_now_unix();
35    let mut ci = crate::connection_identifier::ConnectionIdentifier {
36        version: crate::connection_identifier::CI_VERSION,
37        front_door_did: fdd,
38        identity_pubkey_hex: String::new(),
39        wireguard_pubkey_hex: id.wireguard_pubkey_hex(),
40        overlay_addr: id.overlay_addr(),
41        rendezvous,
42        relation_type,
43        display_name: crate::user_profile::load_profile().display_name,
44        created_at: now,
45        expires_at: now + 7 * 24 * 3600,
46        nonce: uuid::Uuid::new_v4().to_string(),
47        signature_hex: String::new(),
48    };
49    ci.sign(&id.signing_key());
50    Ok(ci)
51}
52
53/// A signed connection identifier for this node (self-certifying front-door DID + WireGuard peering).
54#[cfg(not(target_arch = "wasm32"))]
55pub fn generate_connection_identifier(
56    front_door_did: String,
57    relation_type: String,
58) -> Result<serde_json::Value, String> {
59    let ci = build_signed_identifier(front_door_did, relation_type, "")?;
60    serde_json::to_value(ci).map_err(|e| e.to_string())
61}
62
63/// A magic link (deep link + https + mailto) carrying this node's connection identifier.
64#[cfg(not(target_arch = "wasm32"))]
65pub fn generate_magic_link(
66    front_door_did: String,
67    relation_type: String,
68    domain: String,
69) -> Result<serde_json::Value, String> {
70    let ci = build_signed_identifier(front_door_did, relation_type, &domain)?;
71    let deep = crate::magic_link::to_deep_link(&ci)?;
72    let https = if domain.is_empty() {
73        String::new()
74    } else {
75        crate::magic_link::to_https_link(&ci, &domain)?
76    };
77    let mailto = crate::magic_link::to_mailto(&ci, "Connect with me on Webizen")?;
78    Ok(serde_json::json!({ "deep_link": deep, "https_link": https, "mailto": mailto }))
79}
80
81/// Accept a magic link: parse + **verify** the identifier (self-certifying), then register the sender as a
82/// SocialWebNet peer (their WireGuard peering material). Half of the mutual peering; the return handshake
83/// completes it.
84#[cfg(not(target_arch = "wasm32"))]
85pub fn accept_connection(link: String) -> Result<serde_json::Value, String> {
86    let ci = crate::magic_link::from_link(&link)?;
87    ci.verify()?;
88    if ci.is_expired(mail_now_unix()) {
89        return Err("this connection link has expired".into());
90    }
91    let peer = crate::social_peers::SocialPeer {
92        did: ci.front_door_did.clone(),
93        display_name: ci.display_name.clone(),
94        wireguard_pubkey_hex: ci.wireguard_pubkey_hex.clone(),
95        overlay_addr: ci.overlay_addr.clone(),
96        endpoint: ci
97            .rendezvous
98            .iter()
99            .find(|r| r.kind == "domain" || r.kind == "edge")
100            .map(|r| r.value.clone()),
101        relation_type: ci.relation_type.clone(),
102        added_at: mail_now_unix(),
103        active: true,
104        // Set separately once the peer publishes their envelope key (or via the handshake, later).
105        envelope_pubkey_hex: None,
106    };
107    crate::social_peers::register_peer(peer.clone())?;
108    serde_json::to_value(peer).map_err(|e| e.to_string())
109}
110
111/// The SocialWebNet peers (accepted connections).
112pub fn list_social_peers() -> Result<serde_json::Value, String> {
113    serde_json::to_value(crate::social_peers::list_peers()).map_err(|e| e.to_string())
114}
115
116/// Enable/disable a peer (the socially-defined revoke).
117pub fn set_social_peer_active(did: String, active: bool) -> Result<serde_json::Value, String> {
118    crate::social_peers::set_peer_active(&did, active)?;
119    list_social_peers()
120}
121
122/// Set peer transport endpoint for mesh dial (`host:port`), or clear with empty/None.
123pub fn set_social_peer_endpoint(
124    did: String,
125    endpoint: Option<String>,
126) -> Result<serde_json::Value, String> {
127    crate::social_peers::set_peer_endpoint(&did, endpoint.as_deref())?;
128    list_social_peers()
129}
130
131/// Per-peer mesh dialability — which accepted peers can form a SocialWebNet tunnel now, which must
132/// wait for the peer to reach us (roaming), and which are missing key material. Pure/read-only.
133pub fn mesh_dialability() -> Result<serde_json::Value, String> {
134    let peers = crate::social_peers::list_peers();
135    serde_json::to_value(crate::social_mesh::dialability(&peers)).map_err(|e| e.to_string())
136}