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.

fix: address PR review feedback for relay URL config

- BLOCKER 1: set_relay_client logs warning when OnceLock::set drops value
- BLOCKER 2: save_relay_url logs reqwest::Error before discarding it
- BLOCKER 3: Add RelayConfigError serialization tests (INVALID_URL, UNREACHABLE, KEYCHAIN_ERROR)
- SHOULD FIX 4: normalize_relay_url rejects URLs with non-root paths
- SHOULD FIX 5: Update CLAUDE.md - twelve onboarding screens, seventeen-step state machine
- SHOULD FIX 6: Fix default_relay_url() doc comment to reflect actual usage
- SHOULD FIX 7: Make UTF-8 error logging consistent (tracing::error instead of warn)
- SUGGESTION 8: TypeScript RelayConfigError as discriminated union
- SUGGESTION 9: Add comment explaining test ordering assumption for get_relay_url test
- SUGGESTION 10: Add comment explaining Unreachable conflates transport and HTTP errors

authored by

Malpercio and committed by
Tangled
cf024ab7 a9e9d8fd

+57 -13
+2 -2
apps/identity-wallet/CLAUDE.md
··· 12 12 13 13 **Exposes:** 14 14 - `src/lib/ipc.ts` — typed wrappers for all Tauri IPC commands; import these instead of calling `invoke()` directly. Exports: `createAccount()`, `getOrCreateDeviceKey()`, `signWithDeviceKey()`, `performDIDCeremony()`, `startOAuthFlow()`, `loadHomeData()`, `logOut()`, `getRelayUrl()`, `saveRelayUrl()`, and their associated types (`DevicePublicKey`, `DeviceKeyError`, `CreateAccountResult`, `CreateAccountError`, `DIDCeremonyResult`, `DIDCeremonyError`, `OAuthError`, `SessionInfo`, `HomeData`, `RelayConfigError`) 15 - - `src/lib/components/onboarding/` — eleven onboarding screen components (RelayConfigScreen, WelcomeScreen, ClaimCodeScreen, EmailScreen, HandleScreen, PasswordScreen, LoadingScreen, DIDCeremonyScreen, DIDSuccessScreen, ShamirBackupScreen, AuthenticatingScreen) 15 + - `src/lib/components/onboarding/` — twelve onboarding screen components (RelayConfigScreen, WelcomeScreen, ClaimCodeScreen, EmailScreen, HandleScreen, PasswordScreen, LoadingScreen, DIDCeremonyScreen, DIDSuccessScreen, ShamirBackupScreen, HandleRegistrationScreen, AuthenticatingScreen) 16 16 - `src/lib/components/home/` — three home screen components (HomeScreen, DIDDocumentScreen, RecoveryInfoScreen) plus DIDAvatar utility component (deterministic DID-derived hue circle) 17 - - `src/routes/+page.svelte` — root page: sixteen-step state machine (relay_config -> welcome -> claim_code -> email -> handle -> password -> loading -> did_ceremony -> did_success -> shamir_backup -> handle_registration -> complete -> authenticating -> home -> did_document / recovery_info / auth_failed) 17 + - `src/routes/+page.svelte` — root page: seventeen-step state machine (relay_config -> welcome -> claim_code -> email -> handle -> password -> loading -> did_ceremony -> did_success -> shamir_backup -> handle_registration -> complete -> authenticating -> home -> did_document / recovery_info / auth_failed) 18 18 19 19 **Guarantees:** 20 20 - SSR is disabled globally (`ssr = false` in `src/routes/+layout.ts`); the frontend is a fully static SPA loaded from disk by WKWebView
+3 -3
apps/identity-wallet/src-tauri/src/http.rs
··· 17 17 18 18 /// Returns the compile-time default relay base URL. 19 19 /// 20 - /// Used by integration tests and as the pre-filled default in the relay 21 - /// configuration UI. The runtime URL (from Keychain or user input) takes 22 - /// precedence during normal app operation. 20 + /// Used by integration tests that need the default URL to construct expected 21 + /// endpoint strings. The relay configuration UI hardcodes the same constant 22 + /// independently so the UI does not take a Rust→TS IPC round-trip on mount. 23 23 pub fn default_relay_url() -> &'static str { 24 24 RELAY_BASE_URL 25 25 }
+1 -1
apps/identity-wallet/src-tauri/src/keychain.rs
··· 172 172 match get_item(RELAY_URL_ACCOUNT) { 173 173 Ok(bytes) => String::from_utf8(bytes) 174 174 .map_err(|e| { 175 - tracing::warn!(error = %e, "relay URL in Keychain is not valid UTF-8; treating as absent"); 175 + tracing::error!(error = ?e, "relay URL in Keychain is not valid UTF-8; treating as absent"); 176 176 }) 177 177 .ok(), 178 178 Err(e) if is_not_found(&e) => None,
+40 -3
apps/identity-wallet/src-tauri/src/lib.rs
··· 262 262 if parsed.host().is_none() { 263 263 return Err(RelayConfigError::InvalidUrl); 264 264 } 265 + let path = parsed.path(); 266 + if !path.is_empty() && path != "/" { 267 + return Err(RelayConfigError::InvalidUrl); 268 + } 265 269 Ok(url.trim_end_matches('/').to_string()) 266 270 } 267 271 ··· 631 635 let resp = http::RelayClient::new_with_url(normalized.clone()) 632 636 .get("/xrpc/_health") 633 637 .await 634 - .map_err(|_| RelayConfigError::Unreachable)?; 638 + .map_err(|e| { 639 + tracing::warn!(error = %e, url = %normalized, "relay health check failed"); 640 + RelayConfigError::Unreachable 641 + })?; 635 642 if !resp.status().is_success() { 636 643 tracing::warn!( 637 644 status = %resp.status(), 638 645 url = %normalized, 639 646 "relay health check returned non-success status" 640 647 ); 648 + // Both transport failures (DNS, TLS, timeout) and non-2xx HTTP responses 649 + // map to Unreachable — the frontend only needs to know "can't use this URL". 641 650 return Err(RelayConfigError::Unreachable); 642 651 } 643 652 keychain::store_relay_url(&normalized).map_err(|e| { ··· 1061 1070 assert_eq!(json["code"], "SHARE_STORAGE_FAILED"); 1062 1071 } 1063 1072 1073 + // -- RelayConfigError serialization (one test per variant) -- 1074 + #[test] 1075 + fn relay_config_error_invalid_url_serializes_correctly() { 1076 + let json = serde_json::to_value(RelayConfigError::InvalidUrl).unwrap(); 1077 + assert_eq!(json["code"], "INVALID_URL"); 1078 + } 1079 + 1080 + #[test] 1081 + fn relay_config_error_unreachable_serializes_correctly() { 1082 + let json = serde_json::to_value(RelayConfigError::Unreachable).unwrap(); 1083 + assert_eq!(json["code"], "UNREACHABLE"); 1084 + } 1085 + 1086 + #[test] 1087 + fn relay_config_error_keychain_error_serializes_correctly() { 1088 + let json = serde_json::to_value(RelayConfigError::KeychainError).unwrap(); 1089 + assert_eq!(json["code"], "KEYCHAIN_ERROR"); 1090 + } 1091 + 1064 1092 // -- normalize_relay_url -- 1065 1093 1066 1094 #[test] ··· 1101 1129 )); 1102 1130 } 1103 1131 1132 + #[test] 1133 + fn normalize_relay_url_rejects_urls_with_paths() { 1134 + assert!(matches!( 1135 + normalize_relay_url("https://relay.example.com/api/v1").unwrap_err(), 1136 + RelayConfigError::InvalidUrl 1137 + )); 1138 + } 1139 + 1104 1140 // -- get_relay_url / load_relay_url round-trip -- 1105 1141 1106 1142 #[test] 1107 1143 fn get_relay_url_returns_none_before_save() { 1108 - // The Keychain test mock starts empty in a fresh process; tests that 1109 - // write to the store must clean up via delete_relay_url_test_only(). 1144 + // Relies on the keychain mock starting empty for this key. The sibling test 1145 + // relay_url_round_trips_through_keychain cleans up via delete_relay_url_test_only(), 1146 + // so ordering is not a concern as long as both tests run in the same process. 1110 1147 assert!(get_relay_url().is_none()); 1111 1148 } 1112 1149
+7 -3
apps/identity-wallet/src-tauri/src/oauth.rs
··· 48 48 /// Set the relay client from a runtime URL. Silently ignored if already set 49 49 /// (OnceLock::set semantics — this is only called once on first launch). 50 50 pub fn set_relay_client(&self, url: String) { 51 - self.relay_client 52 - .set(crate::http::RelayClient::new_with_url(url)) 53 - .ok(); 51 + if self 52 + .relay_client 53 + .set(crate::http::RelayClient::new_with_url(url.clone())) 54 + .is_err() 55 + { 56 + tracing::warn!(url = %url, "set_relay_client: relay_client already initialized; ignoring"); 57 + } 54 58 } 55 59 } 56 60
+4 -1
apps/identity-wallet/src/lib/ipc.ts
··· 285 285 * Error from relay URL configuration commands. 286 286 * Serialized as `{ code: "INVALID_URL" }` etc. by the Rust backend. 287 287 */ 288 - export type RelayConfigError = { code: 'INVALID_URL' | 'UNREACHABLE' | 'KEYCHAIN_ERROR' }; 288 + export type RelayConfigError = 289 + | { code: 'INVALID_URL' } 290 + | { code: 'UNREACHABLE' } 291 + | { code: 'KEYCHAIN_ERROR' }; 289 292 290 293 /** 291 294 * Returns the saved relay base URL, or null if not yet configured.