qualia_client_core/
magic_link.rs1use crate::connection_identifier::ConnectionIdentifier;
16
17pub const SCHEME: &str = "web+qualia";
19
20pub fn to_deep_link(id: &ConnectionIdentifier) -> Result<String, String> {
25 let payload = id.encode()?;
26 Ok(format!("{SCHEME}://connect?p={payload}"))
27}
28
29pub fn to_https_link(id: &ConnectionIdentifier, domain: &str) -> Result<String, String> {
36 let payload = id.encode()?;
37 Ok(format!("https://{domain}/w#{payload}"))
38}
39
40pub fn from_link(link: &str) -> Result<ConnectionIdentifier, String> {
49 let link = link.trim();
50
51 let payload = if let Some(rest) = link.strip_prefix(&format!("{SCHEME}://")) {
52 let query = rest.split_once('?').map(|(_, q)| q).unwrap_or("");
54 query
55 .split('&')
56 .find_map(|kv| kv.strip_prefix("p="))
57 .ok_or("deep link missing `p=` payload")?
58 .to_string()
59 } else if let Some((_, frag)) = link.split_once('#') {
60 if frag.is_empty() {
62 return Err("https link has an empty `#` fragment".into());
63 }
64 frag.to_string()
65 } else if link.starts_with("qcx1_") {
66 link.to_string()
68 } else {
69 return Err("unrecognised link: expected a web+qualia:// deep link, an https://…/w# link, or a bare qcx1_ string".into());
70 };
71
72 let payload = pct_decode(&payload);
73 ConnectionIdentifier::decode(&payload)
74}
75
76pub fn to_mailto(id: &ConnectionIdentifier, subject: &str) -> Result<String, String> {
81 let deep = to_deep_link(id)?;
82 let body =
83 format!("You've been invited to connect. Open this link on your device:\n\n{deep}\n");
84 Ok(format!(
85 "mailto:?subject={}&body={}",
86 pct_encode(subject),
87 pct_encode(&body)
88 ))
89}
90
91fn pct_encode(s: &str) -> String {
94 urlencoding::encode(s).into_owned()
95}
96
97fn pct_decode(s: &str) -> String {
100 match urlencoding::decode(s) {
101 Ok(decoded) => decoded.into_owned(),
102 Err(_) => s.to_string(),
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn sample() -> ConnectionIdentifier {
111 ConnectionIdentifier {
112 version: 1,
113 front_door_did: "did:x".into(),
114 identity_pubkey_hex: String::new(),
115 wireguard_pubkey_hex: "aa".repeat(32),
116 overlay_addr: "fd00::1".into(),
117 rendezvous: vec![],
118 relation_type: String::new(),
119 display_name: "Alice".into(),
120 created_at: 1,
121 expires_at: 0,
122 nonce: "n".into(),
123 signature_hex: String::new(),
124 }
125 }
126
127 #[test]
128 fn deep_link_round_trips() {
129 let id = sample();
130 let link = to_deep_link(&id).expect("deep link");
131 assert!(link.starts_with("web+qualia://connect?p="));
132 let back = from_link(&link).expect("decode deep link");
133 assert_eq!(back, id, "deep link round-trips to an equal identifier");
134 }
135
136 #[test]
137 fn https_link_round_trips() {
138 let id = sample();
139 let link = to_https_link(&id, "alice.example").expect("https link");
140 assert!(link.starts_with("https://alice.example/w#"));
141 let back = from_link(&link).expect("decode https link");
142 assert_eq!(back, id, "https link round-trips to an equal identifier");
143 }
144
145 #[test]
146 fn bare_identifier_is_accepted() {
147 let id = sample();
148 let bare = id.encode().expect("encode");
149 assert!(bare.starts_with("qcx1_"));
150 let back = from_link(&bare).expect("decode bare identifier");
151 assert_eq!(back, id, "a bare qcx1_ string is accepted");
152 }
153
154 #[test]
155 fn mailto_contains_the_deep_link() {
156 let id = sample();
157 let deep = to_deep_link(&id).expect("deep link");
158 let mailto = to_mailto(&id, "Connect with Alice").expect("mailto");
159 assert!(mailto.starts_with("mailto:?subject="));
160 assert!(mailto.contains("&body="));
161 let encoded_deep = pct_encode(&deep);
163 assert!(
164 mailto.contains(&encoded_deep),
165 "mailto body contains the (percent-encoded) deep link"
166 );
167 }
168
169 #[test]
170 fn percent_escaped_payload_is_decoded_defensively() {
171 let id = sample();
173 let payload = id.encode().expect("encode");
174 let escaped = payload.replace('a', "%61");
176 let link = format!("{SCHEME}://connect?p={escaped}");
177 let back = from_link(&link).expect("decode percent-escaped payload");
178 assert_eq!(back, id, "percent-escaped payload decodes");
179 }
180
181 #[test]
182 fn unrecognised_link_is_rejected() {
183 assert!(from_link("ftp://nope").is_err());
184 assert!(from_link("just some text").is_err());
185 }
186}