An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(relay-config): add relay_client to AppState and migrate commands to state-based access

Task 1: Add relay_client to AppState
- Add OnceLock<RelayClient> field to AppState in oauth.rs
- Implement relay_client() getter that initializes with compile-time default
- Implement set_relay_client() for runtime URL configuration
- Add default_relay_url() helper function to http.rs for tests and UI defaults

Task 2: Remove RELAY_CLIENT static and update all four commands
- Remove global RELAY_CLIENT static from lib.rs
- Update create_account to accept state and use state.relay_client()
- Update perform_did_ceremony to accept state and use state.relay_client()
- Update register_handle to accept state and use state.relay_client()
- Update check_handle_resolution to accept state and use state.relay_client()
- Change check_handle_resolution return type to Result<bool, String> (required by Tauri for state params)
- Fix test cases in oauth.rs to initialize relay_client field

All OAuth tests pass. Compilation succeeds with zero errors.
App continues to work with compile-time default URL throughout this phase.

authored by

Malpercio and committed by
Tangled
72e6e90a 724c9a17

+56 -18
+9
apps/identity-wallet/src-tauri/src/http.rs
··· 14 14 #[cfg(not(debug_assertions))] 15 15 const RELAY_BASE_URL: &str = "https://relay.ezpds.com"; 16 16 17 + /// Returns the compile-time default relay base URL. 18 + /// 19 + /// Used by integration tests and as the pre-filled default in the relay 20 + /// configuration UI. The runtime URL (from Keychain or user input) takes 21 + /// precedence during normal app operation. 22 + pub fn default_relay_url() -> &'static str { 23 + RELAY_BASE_URL 24 + } 25 + 17 26 /// Successful response from `POST /oauth/par` (RFC 9126 §2.2). 18 27 #[derive(Debug, serde::Deserialize)] 19 28 pub struct ParResponse {
+24 -17
apps/identity-wallet/src-tauri/src/lib.rs
··· 7 7 8 8 use crypto::{build_did_plc_genesis_op_with_external_signer, CryptoError, DidKeyUri}; 9 9 use serde::{Deserialize, Serialize}; 10 - use std::sync::LazyLock; 11 10 use tauri::{Emitter, Manager}; 12 11 use tauri_plugin_deep_link::DeepLinkExt; 13 12 ··· 224 223 did: String, 225 224 } 226 225 227 - // ── Static relay client ───────────────────────────────────────────────────── 228 - 229 - static RELAY_CLIENT: LazyLock<http::RelayClient> = LazyLock::new(http::RelayClient::new); 230 - 231 226 // ── Helpers ───────────────────────────────────────────────────────────────── 232 227 233 228 /// Map a relay 409 error subcode string to a typed `CreateAccountError` variant. ··· 249 244 claim_code: String, 250 245 email: String, 251 246 handle: String, 247 + state: tauri::State<'_, oauth::AppState>, 252 248 ) -> Result<CreateAccountResult, CreateAccountError> { 253 249 // 1. Get or create the device's SE-backed (or simulator-fallback) P-256 key. 254 250 let device_key = device_key::get_or_create().map_err(|e| { ··· 265 261 claim_code, 266 262 }; 267 263 268 - let resp = RELAY_CLIENT 264 + let resp = state 265 + .relay_client() 269 266 .post("/v1/accounts/mobile", &req) 270 267 .await 271 268 .map_err(|e| CreateAccountError::NetworkError { ··· 333 330 async fn perform_did_ceremony( 334 331 handle: String, 335 332 password: String, 333 + state: tauri::State<'_, oauth::AppState>, 336 334 ) -> Result<DIDCeremonyResult, DIDCeremonyError> { 337 335 // Step 1: Get or create the device's P-256 key (serves as rotation key). 338 336 let device_key = device_key::get_or_create().map_err(|e| { ··· 342 340 343 341 // Step 2: Fetch the relay's active signing key (public, no auth required). 344 342 let resp = 345 - RELAY_CLIENT 343 + state 344 + .relay_client() 346 345 .get("/v1/relay/keys") 347 346 .await 348 347 .map_err(|e| DIDCeremonyError::NetworkError { ··· 376 375 &rotation_key, 377 376 &signing_key, 378 377 &handle, 379 - http::RelayClient::base_url(), 378 + state.relay_client().base_url_str(), 380 379 |data| { 381 380 device_key::sign(data) 382 381 .map_err(|e| CryptoError::PlcOperation(format!("device signing failed: {e}"))) ··· 407 406 password, 408 407 }; 409 408 410 - let resp = RELAY_CLIENT 409 + let resp = state 410 + .relay_client() 411 411 .post_with_bearer("/v1/dids", &create_did_req, &pending_token) 412 412 .await 413 413 .map_err(|e| DIDCeremonyError::NetworkError { ··· 472 472 #[tauri::command] 473 473 async fn register_handle( 474 474 handle_label: String, 475 + state: tauri::State<'_, oauth::AppState>, 475 476 ) -> Result<RegisterHandleResult, RegisterHandleError> { 476 477 // Step 1: Fetch the relay's primary user domain. 477 - let resp = RELAY_CLIENT 478 + let resp = state 479 + .relay_client() 478 480 .get("/xrpc/com.atproto.server.describeServer") 479 481 .await 480 482 .map_err(|e| RegisterHandleError::NetworkError { ··· 528 530 handle: full_handle.clone(), 529 531 }; 530 532 531 - let resp = RELAY_CLIENT 533 + let resp = state 534 + .relay_client() 532 535 .post_with_bearer("/v1/handles", &req, &session_token) 533 536 .await 534 537 .map_err(|e| RegisterHandleError::NetworkError { ··· 582 585 /// `did` field). Returns `false` for any other response (handle not yet propagated, relay 583 586 /// unreachable, DID mismatch). Never rejects — callers can safely poll on an interval. 584 587 #[tauri::command] 585 - async fn check_handle_resolution(handle: String, expected_did: String) -> bool { 588 + async fn check_handle_resolution( 589 + handle: String, 590 + expected_did: String, 591 + state: tauri::State<'_, oauth::AppState>, 592 + ) -> Result<bool, String> { 586 593 // ATProto handles are alphanumeric + hyphens + dots — all URL-safe; no percent-encoding needed. 587 594 let path = format!("/xrpc/com.atproto.identity.resolveHandle?handle={handle}"); 588 595 589 - let resp = match RELAY_CLIENT.get(&path).await { 596 + let resp = match state.relay_client().get(&path).await { 590 597 Ok(r) => r, 591 598 Err(e) => { 592 599 tracing::debug!(error = %e, "check_handle_resolution: network error, returning false"); 593 - return false; 600 + return Ok(false); 594 601 } 595 602 }; 596 603 597 604 if !resp.status().is_success() { 598 605 tracing::debug!(status = resp.status().as_u16(), "check_handle_resolution: non-success response, returning false"); 599 - return false; 606 + return Ok(false); 600 607 } 601 608 602 609 match resp.json::<ResolveHandleResponse>().await { 603 - Ok(body) => body.did == expected_did, 610 + Ok(body) => Ok(body.did == expected_did), 604 611 Err(e) => { 605 612 tracing::debug!(error = %e, "check_handle_resolution: failed to parse response, returning false"); 606 - false 613 + Ok(false) 607 614 } 608 615 } 609 616 }
+23 -1
apps/identity-wallet/src-tauri/src/oauth.rs
··· 7 7 use p256::ecdsa::{signature::Signer, Signature, SigningKey}; 8 8 use rand_core::{OsRng, RngCore}; 9 9 use sha2::{Digest, Sha256}; 10 - use std::sync::Mutex; 10 + use std::sync::{Mutex, OnceLock}; 11 11 use std::time::{SystemTime, UNIX_EPOCH}; 12 12 use tracing; 13 13 use uuid::Uuid; ··· 25 25 /// The active authenticated session after a successful token exchange. 26 26 /// Set by `start_oauth_flow` on success; read by `OAuthClient` for every request. 27 27 pub oauth_session: Mutex<Option<OAuthSession>>, 28 + /// Runtime relay client. Populated from Keychain on startup (Phase 3) or by 29 + /// `save_relay_url` on first launch. Falls back to the compile-time default if unset. 30 + relay_client: OnceLock<crate::http::RelayClient>, 28 31 } 29 32 30 33 impl AppState { ··· 32 35 Self { 33 36 pending_auth: Mutex::new(None), 34 37 oauth_session: Mutex::new(None), 38 + relay_client: OnceLock::new(), 35 39 } 40 + } 41 + 42 + /// Returns the configured relay client, or initializes with the compile-time 43 + /// default URL if none has been set yet. 44 + pub fn relay_client(&self) -> &crate::http::RelayClient { 45 + self.relay_client 46 + .get_or_init(crate::http::RelayClient::new) 47 + } 48 + 49 + /// Set the relay client from a runtime URL. Silently ignored if already set 50 + /// (OnceLock::set semantics — this is only called once on first launch). 51 + pub fn set_relay_client(&self, url: String) { 52 + self.relay_client 53 + .set(crate::http::RelayClient::new_with_url(url)) 54 + .ok(); 36 55 } 37 56 } 38 57 ··· 867 886 csrf_state: "correct-state".to_string(), 868 887 })), 869 888 oauth_session: std::sync::Mutex::new(None), 889 + relay_client: OnceLock::new(), 870 890 }; 871 891 872 892 let url = make_test_url("code123", "WRONG-STATE"); ··· 894 914 csrf_state: "good-state".to_string(), 895 915 })), 896 916 oauth_session: std::sync::Mutex::new(None), 917 + relay_client: OnceLock::new(), 897 918 }; 898 919 899 920 // First callback succeeds. ··· 923 944 csrf_state: "expected-state".to_string(), 924 945 })), 925 946 oauth_session: std::sync::Mutex::new(None), 947 + relay_client: OnceLock::new(), 926 948 }; 927 949 928 950 let url = make_test_url("mycode", "expected-state");