Skip to main content

max / synckit-client

Borrow optimizations, Arc<String> token, validate_api_key - AuthRequest/OAuthTokenRequest use &str borrows instead of owned Strings - SessionInfo.token is Arc<String> to avoid cloning on access - New public validate_api_key() for setup UIs to verify keys before saving Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-22 05:29 UTC
Commit: db8a161aae7b1437f2ac1d9c2bdf35e542448edf
Parent: cd3ee08
4 files changed, +63 insertions, -32 deletions
M src/client/auth.rs +18 -18
@@ -24,9 +24,9 @@ impl SyncKitClient {
24 24 password: &str,
25 25 ) -> Result<(Uuid, Uuid)> {
26 26 let body = Bytes::from(serde_json::to_vec(&AuthRequest {
27 - email: email.to_string(),
28 - password: password.to_string(),
29 - api_key: self.config.api_key.clone(),
27 + email,
28 + password,
29 + api_key: &self.config.api_key,
30 30 })?);
31 31
32 32 let resp = self
@@ -139,11 +139,11 @@ impl SyncKitClient {
139 139 let redirect_uri = format!("http://127.0.0.1:{}/", redirect_port);
140 140
141 141 let body = Bytes::from(serde_json::to_vec(&OAuthTokenRequest {
142 - grant_type: "authorization_code".to_string(),
143 - code: code.to_string(),
144 - redirect_uri,
145 - code_verifier: code_verifier.to_string(),
146 - client_id: self.config.api_key.clone(),
142 + grant_type: "authorization_code",
143 + code,
144 + redirect_uri: &redirect_uri,
145 + code_verifier,
146 + client_id: &self.config.api_key,
147 147 })?);
148 148
149 149 let resp = self
@@ -222,7 +222,7 @@ mod tests {
222 222 client.restore_session("fake-token", user_id, app_id).unwrap();
223 223
224 224 let info = client.session_info().unwrap().expect("session should exist");
225 - assert_eq!(info.token, "fake-token");
225 + assert_eq!(*info.token, "fake-token");
226 226 assert_eq!(info.user_id, user_id);
227 227 assert_eq!(info.app_id, app_id);
228 228 }
@@ -236,7 +236,7 @@ mod tests {
236 236 client.restore_session("second-token", user_id, app_id).unwrap();
237 237
238 238 let info = client.session_info().unwrap().unwrap();
239 - assert_eq!(info.token, "second-token");
239 + assert_eq!(*info.token, "second-token");
240 240 }
241 241
242 242 // ── build_authorize_url ──
@@ -352,11 +352,11 @@ mod tests {
352 352 #[test]
353 353 fn oauth_token_request_serialization() {
354 354 let req = OAuthTokenRequest {
355 - grant_type: "authorization_code".to_string(),
356 - code: "auth-code-123".to_string(),
357 - redirect_uri: "http://127.0.0.1:8080/".to_string(),
358 - code_verifier: "verifier-abc".to_string(),
359 - client_id: "api-key".to_string(),
355 + grant_type: "authorization_code",
356 + code: "auth-code-123",
357 + redirect_uri: "http://127.0.0.1:8080/",
358 + code_verifier: "verifier-abc",
359 + client_id: "api-key",
360 360 };
361 361
362 362 let json = serde_json::to_string(&req).unwrap();
@@ -387,9 +387,9 @@ mod tests {
387 387 #[test]
388 388 fn auth_request_serialization() {
389 389 let req = AuthRequest {
390 - email: "user@example.com".to_string(),
391 - password: "secret123".to_string(),
392 - api_key: "ak_test".to_string(),
390 + email: "user@example.com",
391 + password: "secret123",
392 + api_key: "ak_test",
393 393 };
394 394
395 395 let json = serde_json::to_string(&req).unwrap();
@@ -127,8 +127,8 @@ struct Session {
127 127
128 128 /// Public session info returned by `session_info()`.
129 129 pub struct SessionInfo {
130 - /// The JWT bearer token for API requests.
131 - pub token: String,
130 + /// The JWT bearer token for API requests (shared ref-counted to avoid cloning).
131 + pub token: Arc<String>,
132 132 /// The authenticated user's UUID.
133 133 pub user_id: Uuid,
134 134 /// The SyncKit app UUID this session belongs to.
@@ -192,7 +192,7 @@ impl SyncKitClient {
192 192 pub fn session_info(&self) -> Result<Option<SessionInfo>> {
193 193 let guard = self.session.read();
194 194 Ok(guard.as_ref().map(|s| SessionInfo {
195 - token: (*s.token).clone(),
195 + token: Arc::clone(&s.token),
196 196 user_id: s.user_id,
197 197 app_id: s.app_id,
198 198 }))
@@ -250,6 +250,37 @@ impl SyncKitClient {
250 250 }
251 251 }
252 252
253 + /// Validate an API key against a SyncKit server without constructing a full client.
254 + ///
255 + /// Returns the app name on success, or an error if the key is invalid or the server
256 + /// is unreachable. This is intended for setup UIs that need to verify a key before
257 + /// saving it.
258 + pub async fn validate_api_key(server_url: &str, api_key: &str) -> Result<String> {
259 + let url = format!("{server_url}/api/sync/validate-app");
260 + let http = reqwest::Client::builder()
261 + .timeout(std::time::Duration::from_secs(10))
262 + .build()?;
263 + let resp = http
264 + .get(&url)
265 + .query(&[("api_key", api_key)])
266 + .send()
267 + .await?;
268 + let status = resp.status().as_u16();
269 + if status == 401 {
270 + return Err(SyncKitError::Server {
271 + status: 401,
272 + message: "Invalid API key".to_string(),
273 + });
274 + }
275 + let resp = helpers::check_response(resp).await?;
276 + #[derive(serde::Deserialize)]
277 + struct ValidateResponse {
278 + app_name: String,
279 + }
280 + let body: ValidateResponse = resp.json().await?;
281 + Ok(body.app_name)
282 + }
283 +
253 284 #[cfg(test)]
254 285 mod tests {
255 286 use super::*;
M src/lib.rs +1 -1
@@ -48,6 +48,6 @@ pub mod keystore;
48 48 pub mod types;
49 49
50 50 // Re-exports for convenience
51 - pub use client::{SessionInfo, SyncKitClient, SyncKitConfig};
51 + pub use client::{validate_api_key, SessionInfo, SyncKitClient, SyncKitConfig};
52 52 pub use error::{Result, SyncKitError};
53 53 pub use types::{ChangeEntry, ChangeOp, Device, SyncStatus};
M src/types.rs +10 -10
@@ -47,10 +47,10 @@ impl ChangeOp {
47 47 // ── Auth ──
48 48
49 49 #[derive(Serialize)]
50 - pub(crate) struct AuthRequest {
51 - pub email: String,
52 - pub password: String,
53 - pub api_key: String,
50 + pub(crate) struct AuthRequest<'a> {
51 + pub email: &'a str,
52 + pub password: &'a str,
53 + pub api_key: &'a str,
54 54 }
55 55
56 56 #[derive(Deserialize)]
@@ -175,12 +175,12 @@ pub(crate) struct GetKeyResponse {
175 175 // ── OAuth ──
176 176
177 177 #[derive(Serialize)]
178 - pub(crate) struct OAuthTokenRequest {
179 - pub grant_type: String,
180 - pub code: String,
181 - pub redirect_uri: String,
182 - pub code_verifier: String,
183 - pub client_id: String,
178 + pub(crate) struct OAuthTokenRequest<'a> {
179 + pub grant_type: &'a str,
180 + pub code: &'a str,
181 + pub redirect_uri: &'a str,
182 + pub code_verifier: &'a str,
183 + pub client_id: &'a str,
184 184 }
185 185
186 186 #[derive(Deserialize)]