max / synckit
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
1 file changed,
+20 insertions,
-0 deletions
| @@ -97,6 +97,13 @@ | |||
| 97 | 97 | /// an idle notification stream is never mistaken for a stall. | |
| 98 | 98 | const STREAM_READ_TIMEOUT: Duration = Duration::from_secs(90); | |
| 99 | 99 | ||
| 100 | + | /// User-Agent sent on every request this SDK makes. The version is the only | |
| 101 | + | /// thing identified: no OS string, no host name, no consumer-app name. It | |
| 102 | + | /// answers "which SyncKit is actually in the field", which a lockfile read at | |
| 103 | + | /// release time cannot. The server persists it per registered device (see | |
| 104 | + | /// `sync_devices.client_version`), it does not log it per request. | |
| 105 | + | const USER_AGENT: &str = concat!("synckit-client/", env!("CARGO_PKG_VERSION")); | |
| 106 | + | ||
| 100 | 107 | /// Configuration for the SyncKit client. | |
| 101 | 108 | #[derive(Debug, Clone)] | |
| 102 | 109 | pub struct SyncKitConfig { | |
| @@ -342,6 +349,7 @@ | |||
| 342 | 349 | ||
| 343 | 350 | let https_only = requires_https(&config.server_url); | |
| 344 | 351 | let http = Client::builder() | |
| 352 | + | .user_agent(USER_AGENT) | |
| 345 | 353 | .timeout(Duration::from_secs(30)) | |
| 346 | 354 | .connect_timeout(Duration::from_secs(10)) | |
| 347 | 355 | .pool_max_idle_per_host(5) | |
| @@ -359,6 +367,7 @@ | |||
| 359 | 367 | // above the server's 30s SSE keepalive, so an idle notification stream | |
| 360 | 368 | // (which receives a keepalive comment every 30s) is never torn down. | |
| 361 | 369 | let http_stream = Client::builder() | |
| 370 | + | .user_agent(USER_AGENT) | |
| 362 | 371 | .connect_timeout(Duration::from_secs(10)) | |
| 363 | 372 | .read_timeout(STREAM_READ_TIMEOUT) | |
| 364 | 373 | .pool_max_idle_per_host(5) | |
| @@ -529,6 +538,7 @@ | |||
| 529 | 538 | pub async fn validate_api_key(server_url: &str, api_key: &str) -> Result<String> { | |
| 530 | 539 | let url = format!("{server_url}/api/v1/sync/validate-app"); | |
| 531 | 540 | let http = reqwest::Client::builder() | |
| 541 | + | .user_agent(USER_AGENT) | |
| 532 | 542 | .timeout(std::time::Duration::from_secs(10)) | |
| 533 | 543 | .https_only(requires_https(server_url)) | |
| 534 | 544 | .build()?; | |
| @@ -566,6 +576,16 @@ | |||
| 566 | 576 | use super::*; | |
| 567 | 577 | use base64::Engine; | |
| 568 | 578 | ||
| 579 | + | #[test] | |
| 580 | + | fn user_agent_is_name_slash_version() { | |
| 581 | + | // The server parses on the `/` and stores the right-hand side, so the | |
| 582 | + | // shape is a wire contract, not cosmetic. | |
| 583 | + | let (name, version) = USER_AGENT.split_once('/').expect("no `/` in User-Agent"); | |
| 584 | + | assert_eq!(name, "synckit-client"); | |
| 585 | + | assert_eq!(version, env!("CARGO_PKG_VERSION")); | |
| 586 | + | assert!(version.starts_with(char::is_numeric)); | |
| 587 | + | } | |
| 588 | + | ||
| 569 | 589 | #[test] | |
| 570 | 590 | fn requires_https_enforces_tls_off_loopback() { | |
| 571 | 591 | // https or non-loopback http must enforce TLS. |