difflore_core/cloud/
endpoints.rs1pub const DEFAULT_API_BASE: &str = "https://difflore.dev/api";
12
13pub const ENV_CLOUD_URL: &str = crate::env::DIFFLORE_CLOUD_URL;
16
17pub const LEGACY_ENV_CLOUD_URL: &str = crate::env::DIFF_LORE_CLOUD_URL;
19
20pub fn api_base() -> String {
24 env_api_base(ENV_CLOUD_URL)
25 .or_else(|| env_api_base(LEGACY_ENV_CLOUD_URL))
26 .unwrap_or_else(|| DEFAULT_API_BASE.to_owned())
27}
28
29fn env_api_base(name: &str) -> Option<String> {
30 crate::env::var(name)
31 .map(|v| v.trim().to_owned())
32 .filter(|v| !v.is_empty())
33}
34
35pub fn web_origin() -> String {
38 web_origin_from(&api_base())
39}
40
41pub fn web_origin_from(api_base: &str) -> String {
44 let trimmed = api_base.trim_end_matches('/');
45 trimmed
46 .strip_suffix("/api")
47 .map_or_else(|| trimmed.to_owned(), ToOwned::to_owned)
48}
49
50pub fn api_origin() -> String {
53 origin_of(&api_base())
54}
55
56pub fn default_api_origin() -> String {
58 origin_of(DEFAULT_API_BASE)
59}
60
61pub fn origin_of(url: &str) -> String {
64 let trimmed = url.trim();
65 let Some((scheme, rest)) = trimmed.split_once("://") else {
66 return trimmed.to_owned();
67 };
68 let authority = rest.split('/').next().unwrap_or(rest);
69 format!(
72 "{}://{}",
73 scheme.to_ascii_lowercase(),
74 authority.to_ascii_lowercase()
75 )
76}
77
78pub fn web_link(path: &str) -> String {
81 web_link_from(&api_base(), path)
82}
83
84pub fn web_link_from(api_base: &str, path: &str) -> String {
86 let base = web_origin_from(api_base);
87 let path = path.trim_start_matches('/');
88 if path.is_empty() {
89 base
90 } else {
91 format!("{base}/{path}")
92 }
93}
94
95pub fn web_host_display() -> String {
97 let origin = web_origin();
98 origin
99 .strip_prefix("https://")
100 .or_else(|| origin.strip_prefix("http://"))
101 .unwrap_or(&origin)
102 .to_owned()
103}
104
105pub fn pricing_url() -> String {
107 web_link("pricing")
108}
109
110pub const GITHUB_REPO: &str = "difflore/difflore-cli";
116
117pub fn github_repo_url() -> String {
119 format!("https://github.com/{GITHUB_REPO}")
120}
121
122pub fn github_issues_url() -> String {
125 format!("https://github.com/{GITHUB_REPO}/issues")
126}
127
128pub fn github_release_tag_url(version: &str) -> String {
131 format!("https://github.com/{GITHUB_REPO}/releases/tag/v{version}")
132}
133
134use openapi_contract::api;
137
138use super::api_types::RegisterDeviceResult;
139use super::client::CloudClient;
140
141pub async fn register(
142 client: &CloudClient,
143 name: &str,
144 platform: &str,
145) -> crate::Result<RegisterDeviceResult> {
146 let payload = serde_json::json!({ "name": name, "platform": platform });
147 let device: RegisterDeviceResult = api!(POST "/auth/devices", body = &payload)
148 .fetch(client)
149 .await?;
150 Ok(device)
151}
152
153pub const fn detect_platform() -> &'static str {
154 if cfg!(target_os = "windows") {
155 "windows"
156 } else if cfg!(target_os = "macos") {
157 "macos"
158 } else {
159 "linux"
160 }
161}
162
163pub fn detect_hostname() -> String {
164 for key in ["COMPUTERNAME", "HOSTNAME", "HOST"] {
165 if let Some(name) = crate::env::var(key) {
166 return name;
167 }
168 }
169 if !cfg!(target_os = "windows")
171 && let Ok(out) = std::process::Command::new("hostname").output()
172 && out.status.success()
173 {
174 let name = String::from_utf8_lossy(&out.stdout).trim().to_owned();
175 if !name.is_empty() {
176 return name;
177 }
178 }
179 "unknown-device".to_owned()
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn web_origin_strips_trailing_api_segment() {
188 assert_eq!(
189 web_origin_from("https://difflore.dev/api"),
190 "https://difflore.dev"
191 );
192 assert_eq!(
193 web_origin_from("https://difflore.dev/api/"),
194 "https://difflore.dev"
195 );
196 }
197
198 #[test]
199 fn web_origin_passes_through_when_no_api_suffix() {
200 assert_eq!(
201 web_origin_from("https://difflore.dev"),
202 "https://difflore.dev"
203 );
204 }
205
206 #[test]
207 fn origin_of_keeps_scheme_and_authority_drops_path() {
208 assert_eq!(
209 origin_of("https://difflore.dev/api"),
210 "https://difflore.dev"
211 );
212 assert_eq!(
213 origin_of("https://difflore.dev/api/"),
214 "https://difflore.dev"
215 );
216 assert_eq!(
217 origin_of("http://127.0.0.1:3017/api"),
218 "http://127.0.0.1:3017"
219 );
220 assert_eq!(
221 origin_of("https://staging.example.com:8443/api/v1"),
222 "https://staging.example.com:8443"
223 );
224 assert_eq!(
225 origin_of(" https://difflore.dev/api "),
226 "https://difflore.dev"
227 );
228 }
229
230 #[test]
231 fn origin_of_distinguishes_scheme_and_host_so_tokens_cannot_cross() {
232 assert_ne!(
234 origin_of("https://difflore.dev/api"),
235 origin_of("http://difflore.dev/api")
236 );
237 assert_ne!(
238 origin_of("https://difflore.dev/api"),
239 origin_of("https://attacker.example/api")
240 );
241 }
242
243 #[test]
244 fn origin_of_is_case_insensitive_on_scheme_and_host() {
245 assert_eq!(
247 origin_of("HTTPS://Difflore.DEV/api"),
248 origin_of("https://difflore.dev/api"),
249 );
250 assert_eq!(
251 origin_of("HTTPS://Difflore.DEV/api"),
252 "https://difflore.dev"
253 );
254 }
255
256 #[test]
257 fn default_api_origin_is_the_production_host() {
258 assert_eq!(default_api_origin(), "https://difflore.dev");
259 }
260
261 #[test]
262 fn web_link_handles_leading_slash_either_way() {
263 assert_eq!(
264 web_link_from("https://difflore.dev/api", "/pricing"),
265 "https://difflore.dev/pricing"
266 );
267 assert_eq!(
268 web_link_from("https://difflore.dev/api", "pricing"),
269 "https://difflore.dev/pricing"
270 );
271 assert_eq!(
272 web_link_from("https://difflore.dev/api", ""),
273 "https://difflore.dev"
274 );
275 }
276
277 #[test]
278 fn api_base_honors_canonical_cloud_url_env() {
279 temp_env::with_vars(
280 [
281 (ENV_CLOUD_URL, Some(" http://127.0.0.1:3017/api ")),
282 (LEGACY_ENV_CLOUD_URL, None),
283 ],
284 || {
285 assert_eq!(api_base(), "http://127.0.0.1:3017/api");
286 },
287 );
288 }
289
290 #[test]
291 fn api_base_honors_legacy_cloud_url_env() {
292 temp_env::with_vars(
293 [
294 (ENV_CLOUD_URL, None),
295 (LEGACY_ENV_CLOUD_URL, Some(" http://127.0.0.1:3018/api ")),
296 ],
297 || {
298 assert_eq!(api_base(), "http://127.0.0.1:3018/api");
299 },
300 );
301 }
302
303 #[test]
304 fn canonical_cloud_url_env_wins_over_legacy() {
305 temp_env::with_vars(
306 [
307 (ENV_CLOUD_URL, Some("http://127.0.0.1:3017/api")),
308 (LEGACY_ENV_CLOUD_URL, Some("http://127.0.0.1:3018/api")),
309 ],
310 || {
311 assert_eq!(api_base(), "http://127.0.0.1:3017/api");
312 },
313 );
314 }
315}