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 MM-148 handle registration

Critical:
- Fix setInterval async callback swallowing poll errors — use .then/.catch
so IPC failures are logged instead of silently dropped for 2 minutes
- Map HTTP 401 from /v1/handles to SessionExpired (not KeychainError);
401 means the relay rejected the token, not a Keychain read failure

Important:
- Replace serde_json::Value dns_status parsing with typed CreateHandleRelayResponse
struct so dns_status deserialization is explicit, not a silent unwrap_or fallback
- Add tracing::debug\! for non-2xx in check_handle_resolution so relay 500s
during polling leave per-tick evidence in logs
- Add console.error in run() catch block so UNKNOWN errors are observable
- Change RegisterHandleError TypeScript type to a proper discriminated union
matching the OAuthError pattern (message required on NETWORK_ERROR/UNKNOWN)

Suggestions:
- Add settled guard to prevent onsuccess/ontimeout racing if a poll tick
resolves in-flight when the timeout fires
- Upgrade tracing::warn\! → tracing::error\! for missing DID in Keychain
(post-ceremony invariant violation, not a routine warn)

authored by

Malpercio and committed by
Tangled
04ad49c5 f2a11a7a

+64 -31
+25 -11
apps/identity-wallet/src-tauri/src/lib.rs
··· 177 177 handle: String, 178 178 } 179 179 180 + /// Success response from `POST /v1/handles`. 181 + #[derive(Deserialize)] 182 + struct CreateHandleRelayResponse { 183 + dns_status: String, 184 + } 185 + 180 186 /// Successful result returned to the Svelte frontend after handle registration. 181 187 #[derive(Serialize)] 182 188 #[serde(rename_all = "camelCase")] ··· 200 206 DnsError, 201 207 #[error("keychain operation failed")] 202 208 KeychainError, 209 + /// The relay rejected the session token (401). The token is expired or revoked — the user 210 + /// must re-authenticate via OAuth rather than restart the app. 211 + #[error("session token expired or revoked")] 212 + SessionExpired, 203 213 #[error("relay has no user domains configured")] 204 214 NoDomains, 205 215 #[error("network error: {message}")] ··· 493 503 let full_handle = format!("{handle_label}.{domain}"); 494 504 495 505 // Step 2: Read DID and session token from Keychain. 506 + // Missing DID here is a post-ceremony invariant violation — error! is appropriate. 496 507 let did_bytes = keychain::get_item("did").map_err(|e| { 497 - tracing::warn!(error = %e, "failed to read DID from Keychain during handle registration"); 508 + tracing::error!(error = %e, "DID not found in Keychain during handle registration — ceremony invariant violated"); 498 509 RegisterHandleError::KeychainError 499 510 })?; 500 511 let did = String::from_utf8(did_bytes).map_err(|e| { 501 - tracing::warn!(error = %e, "DID bytes are not valid UTF-8"); 512 + tracing::error!(error = %e, "DID bytes are not valid UTF-8"); 502 513 RegisterHandleError::KeychainError 503 514 })?; 504 515 ··· 527 538 let status = resp.status(); 528 539 529 540 if status.is_success() { 530 - // Relay returns { "handle": "...", "dns_status": "...", "did": "..." }. 531 - // We only need handle and dns_status for the result. 532 - let body: serde_json::Value = 541 + let body: CreateHandleRelayResponse = 533 542 resp.json() 534 543 .await 535 544 .map_err(|e| RegisterHandleError::Unknown { 536 545 message: format!("failed to parse /v1/handles response: {e}"), 537 546 })?; 538 - let dns_status = body["dns_status"] 539 - .as_str() 540 - .unwrap_or("not_configured") 541 - .to_string(); 542 547 Ok(RegisterHandleResult { 543 548 handle: full_handle, 544 - dns_status, 549 + dns_status: body.dns_status, 545 550 }) 546 551 } else { 547 552 match status.as_u16() { ··· 558 563 }) 559 564 } 560 565 } 561 - 401 => Err(RegisterHandleError::KeychainError), 566 + // 401 means the relay rejected the session token — it's expired or revoked. 567 + // The Keychain read already succeeded; this is an auth problem, not a Keychain problem. 568 + 401 => Err(RegisterHandleError::SessionExpired), 562 569 409 => Err(RegisterHandleError::HandleTaken), 563 570 502 => Err(RegisterHandleError::DnsError), 564 571 other => Err(RegisterHandleError::NetworkError { ··· 588 595 }; 589 596 590 597 if !resp.status().is_success() { 598 + tracing::debug!(status = resp.status().as_u16(), "check_handle_resolution: non-success response, returning false"); 591 599 return false; 592 600 } 593 601 ··· 832 840 fn register_handle_error_keychain_error_serializes_correctly() { 833 841 let json = serde_json::to_value(&RegisterHandleError::KeychainError).unwrap(); 834 842 assert_eq!(json["code"], "KEYCHAIN_ERROR"); 843 + } 844 + 845 + #[test] 846 + fn register_handle_error_session_expired_serializes_correctly() { 847 + let json = serde_json::to_value(&RegisterHandleError::SessionExpired).unwrap(); 848 + assert_eq!(json["code"], "SESSION_EXPIRED"); 835 849 } 836 850 837 851 #[test]
+29 -9
apps/identity-wallet/src/lib/components/onboarding/HandleRegistrationScreen.svelte
··· 35 35 36 36 let pollTimer: ReturnType<typeof setInterval> | undefined; 37 37 let timeoutTimer: ReturnType<typeof setTimeout> | undefined; 38 + // Guard against the rare race where a poll tick resolves after the timeout fires. 39 + let settled = false; 38 40 39 41 function stopPolling() { 40 42 if (pollTimer !== undefined) { ··· 49 51 50 52 function startPolling(handle: string) { 51 53 phase = { kind: 'polling', handle }; 54 + settled = false; 52 55 53 - pollTimer = setInterval(async () => { 54 - const resolved = await checkHandleResolution(handle, did); 55 - if (resolved) { 56 - stopPolling(); 57 - onsuccess(handle); 58 - } 56 + pollTimer = setInterval(() => { 57 + checkHandleResolution(handle, did) 58 + .then((resolved) => { 59 + if (resolved && !settled) { 60 + settled = true; 61 + stopPolling(); 62 + onsuccess(handle); 63 + } 64 + }) 65 + .catch((err) => { 66 + console.error('[HandleRegistrationScreen] poll tick failed:', err); 67 + }); 59 68 }, POLL_INTERVAL_MS); 60 69 61 70 timeoutTimer = setTimeout(() => { 62 - stopPolling(); 63 - ontimeout(handle); 71 + if (!settled) { 72 + settled = true; 73 + stopPolling(); 74 + ontimeout(handle); 75 + } 64 76 }, POLL_TIMEOUT_MS); 65 77 } 66 78 ··· 72 84 const result = await registerHandle(handleLabel); 73 85 startPolling(result.handle); 74 86 } catch (raw: unknown) { 87 + console.error('[HandleRegistrationScreen] registerHandle failed:', raw); 75 88 if ( 76 89 typeof raw === 'object' && 77 90 raw !== null && ··· 95 108 return 'Handle registered, but DNS setup failed. Please contact support.'; 96 109 case 'NO_DOMAINS': 97 110 return 'The relay has no handle domains configured. Please contact support.'; 111 + case 'SESSION_EXPIRED': 112 + return 'Your session has expired. Please sign in again to continue.'; 98 113 case 'KEYCHAIN_ERROR': 99 114 return "Couldn't read your credentials. Please restart the app and try again."; 100 115 case 'NETWORK_ERROR': ··· 104 119 } 105 120 106 121 function canRetry(err: RegisterHandleError): boolean { 107 - return err.code !== 'INVALID_HANDLE' && err.code !== 'DNS_ERROR' && err.code !== 'NO_DOMAINS'; 122 + return ( 123 + err.code !== 'INVALID_HANDLE' && 124 + err.code !== 'DNS_ERROR' && 125 + err.code !== 'NO_DOMAINS' && 126 + err.code !== 'SESSION_EXPIRED' 127 + ); 108 128 } 109 129 110 130 onMount(() => run());
+10 -11
apps/identity-wallet/src/lib/ipc.ts
··· 172 172 /** 173 173 * Error returned by the `register_handle` Rust command. 174 174 * Serialized as `{ code: "HANDLE_TAKEN" }` etc. by the Rust backend. 175 + * Variants that carry a message have it as a required field on their branch. 175 176 */ 176 - export type RegisterHandleError = { 177 - code: 178 - | 'HANDLE_TAKEN' 179 - | 'INVALID_HANDLE' 180 - | 'DNS_ERROR' 181 - | 'KEYCHAIN_ERROR' 182 - | 'NO_DOMAINS' 183 - | 'NETWORK_ERROR' 184 - | 'UNKNOWN'; 185 - message?: string; 186 - }; 177 + export type RegisterHandleError = 178 + | { code: 'HANDLE_TAKEN' } 179 + | { code: 'INVALID_HANDLE' } 180 + | { code: 'DNS_ERROR' } 181 + | { code: 'KEYCHAIN_ERROR' } 182 + | { code: 'SESSION_EXPIRED' } 183 + | { code: 'NO_DOMAINS' } 184 + | { code: 'NETWORK_ERROR'; message: string } 185 + | { code: 'UNKNOWN'; message: string }; 187 186 188 187 /** 189 188 * Register the user's handle with the relay.