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.

docs: update identity-wallet CLAUDE.md for claim flow commands

Document the new claim.rs module (5 Tauri IPC commands), ClaimState on
AppState, PdsClient.fetch_audit_log and post_plc_operation methods,
TypeScript IPC wrappers and types, error enums, and key design decisions
for the PLC rotation key claim flow.

authored by

Malpercio and committed by
Tangled
2f9873db b88f5713

+26 -9
+26 -9
apps/identity-wallet/CLAUDE.md
··· 1 1 # Identity Wallet Mobile App 2 2 3 - Last verified: 2026-03-28 4 - Last updated: 2026-03-28 3 + Last verified: 2026-03-29 4 + Last updated: 2026-03-29 5 5 6 6 ## Purpose 7 7 ··· 12 12 ### Frontend (SvelteKit 2 + Svelte 5) 13 13 14 14 **Exposes:** 15 - - `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/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()`, `resolveIdentity()`, `startPdsAuth()`, `requestClaimVerification()`, `signAndVerifyClaim()`, `submitClaim()`, and their associated types (`DevicePublicKey`, `DeviceKeyError`, `CreateAccountResult`, `CreateAccountError`, `DIDCeremonyResult`, `DIDCeremonyError`, `OAuthError`, `SessionInfo`, `HomeData`, `RelayConfigError`, `IdentityInfo`, `VerifiedClaimOp`, `OpDiff`, `ServiceChange`, `ClaimResult`, `ResolveError`, `ClaimError`) 16 16 - `src/lib/components/onboarding/` — twelve onboarding screen components (RelayConfigScreen, WelcomeScreen, ClaimCodeScreen, EmailScreen, HandleScreen, PasswordScreen, LoadingScreen, DIDCeremonyScreen, DIDSuccessScreen, ShamirBackupScreen, HandleRegistrationScreen, AuthenticatingScreen) 17 17 - `src/lib/components/home/` — three home screen components (HomeScreen, DIDDocumentScreen, RecoveryInfoScreen) plus DIDAvatar utility component (deterministic DID-derived hue circle) 18 18 - `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) ··· 35 35 - `src/lib.rs::sign_with_device_key(data: Vec<u8>) -> Result<Vec<u8>, DeviceKeyError>` — Tauri IPC command: delegates to `device_key::sign()` 36 36 - `src/lib.rs::perform_did_ceremony(handle: String, password: String) -> Result<DIDCeremonyResult, DIDCeremonyError>` — Tauri IPC command: fetches relay signing key (GET /v1/relay/keys), builds signed did:plc genesis op via `crypto::build_did_plc_genesis_op_with_external_signer` using device key as signer, POSTs genesis op + password to relay (POST /v1/dids with Bearer token), persists DID + upgraded session token + Share 1 in Keychain, returns `{ did, share3 }` to frontend 37 37 - `src/home.rs` — Home screen data module: `load_home_data(AppState) -> Result<HomeData, String>` (Tauri IPC command: fires GET /xrpc/_health and GET /xrpc/com.atproto.server.getSession concurrently via OAuthClient; always succeeds -- partial failures encoded as HomeData fields); `log_out(AppState) -> Result<(), String>` (Tauri IPC command: deletes oauth-access-token, oauth-refresh-token, and did from Keychain, clears in-memory oauth_session; always succeeds -- Keychain errors swallowed); output types: `HomeData` { relay_healthy, session, session_error, share1_in_keychain }, `SessionInfo` { did, handle, email, email_confirmed, did_doc } 38 - - `src/oauth.rs` — OAuth PKCE client module: `AppState` (pending_auth + oauth_session mutexes + relay_client OnceLock + pds_client), `OAuthSession` (access/refresh/expiry/nonce), `DPoPKeypair` (P-256, persisted in Keychain), `OAuthError` enum, PKCE utilities (verifier + S256 challenge), `start_oauth_flow` (Tauri IPC command: DPoP keygen, PKCE, PAR, Safari redirect, deep-link callback, token exchange), `handle_deep_link` (routes deep-link URLs to pending flow); `AppState::pds_client()` accessor exposes `PdsClient` for Phase 4 Tauri commands 38 + - `src/oauth.rs` — OAuth PKCE client module: `AppState` (pending_auth + oauth_session mutexes + relay_client OnceLock + pds_client + claim_state), `OAuthSession` (access/refresh/expiry/nonce), `DPoPKeypair` (P-256, persisted in Keychain), `OAuthError` enum, PKCE utilities (verifier + S256 challenge), `start_oauth_flow` (Tauri IPC command: DPoP keygen, PKCE, PAR, Safari redirect, deep-link callback, token exchange), `handle_deep_link` (routes deep-link URLs to pending flow); `AppState::pds_client()` accessor exposes `PdsClient` for claim flow commands 39 + - `src/claim.rs` — PLC rotation key claim flow module (5 Tauri IPC commands): `resolve_identity(handle_or_did) -> Result<IdentityInfo, ResolveError>` (resolves handle/DID to identity info via plc.directory, stores ClaimState), `start_pds_auth(pds_url) -> Result<(), ClaimError>` (OAuth PKCE+DPoP to arbitrary PDS, stores OAuthClient in ClaimState, emits `"pds_auth_ready"` event), `request_claim_verification(did) -> Result<(), ClaimError>` (calls `requestPlcOperationSignature` XRPC on old PDS to trigger email verification), `sign_and_verify_claim(device_key_id, token) -> Result<VerifiedClaimOp, ClaimError>` (calls `getRecommendedDidCredentials` and `signPlcOperation` on old PDS, fetches audit log, verifies signature + 4-point local checks, stores signed op in ClaimState), `submit_claim(did) -> Result<ClaimResult, ClaimError>` (POSTs signed op to plc.directory, persists identity to IdentityStore, clears ClaimState). Types: `IdentityInfo`, `VerifiedClaimOp`, `OpDiff`, `ServiceChange`, `ClaimResult`, `ClaimState`. Error enums: `ResolveError` (HANDLE_NOT_FOUND, DID_NOT_FOUND, PDS_UNREACHABLE, NETWORK_ERROR), `ClaimError` (INVALID_TOKEN, VERIFICATION_FAILED, PLC_DIRECTORY_ERROR, UNAUTHORIZED, NETWORK_ERROR) 39 40 - `src/oauth_client.rs` — `OAuthClient`: authenticated HTTP client wrapping every request with `Authorization: DPoP {access_token}` + `DPoP` proof headers; transparent lazy refresh when token has <60s remaining; automatic retry on `use_dpop_nonce` 400 responses; methods: `get(path)`, `post(path, body)` 40 41 - `src/device_key.rs` — P-256 device key management with `#[cfg]`-based dispatch: macOS/simulator uses software keys via `crypto` crate + Keychain storage; real iOS device uses Secure Enclave via `security-framework`. Public API: `get_or_create() -> Result<DevicePublicKey, DeviceKeyError>` (idempotent), `sign(data) -> Result<Vec<u8>, DeviceKeyError>` 41 42 - `src/keychain.rs` — iOS Keychain abstraction (`store_item`, `get_item`, `delete_item`) under service `"ezpds-identity-wallet"`; Relay URL helpers: `store_relay_url`/`load_relay_url` (relay base URL); OAuth helpers: `store_dpop_key`/`load_dpop_key` (P-256 DPoP private key scalar), `store_oauth_tokens`/`load_oauth_tokens` (access + refresh token pair) 42 43 - `src/http.rs` — `RelayClient` with runtime-configurable base URL (initialized via `AppState::set_relay_client(url)` on first launch; localhost:8080 debug fallback); methods: `post()`, `get()`, `post_with_bearer()`, `par()` (POST /oauth/par with DPoP proof), `token_exchange()` (POST /oauth/token with PKCE verifier); response types: `ParResponse`, `TokenResponse`, `TokenErrorResponse` 43 44 - `src/identity_store.rs` — `IdentityStore` unit struct for multi-identity Keychain management with per-DID namespacing. Public API: `add_identity(did)` (registers DID in managed-dids index), `remove_identity(did)` (deletes DID and all per-DID entries), `list_identities()` (returns managed DIDs), `get_or_create_device_key(did)` (lazy per-DID P-256 key generation), `store_did_doc(did, json)` / `get_did_doc(did)` (DID document persistence), `store_plc_log(did, json)` / `get_plc_log(did)` (PLC audit log persistence). All methods require DID to be registered first (returns `IdentityNotFound` otherwise). `IdentityStoreError` enum: IDENTITY_NOT_FOUND, IDENTITY_ALREADY_EXISTS, KEYCHAIN_ERROR, KEY_GENERATION_FAILED, SERIALIZATION_ERROR (serialized as `{ code: "SCREAMING_SNAKE_CASE" }`) 44 - - `src/pds_client.rs` — PDS discovery and OAuth module for arbitrary PDS endpoints (not just our relay). `PdsClient` struct (stateless, wraps a `reqwest::Client` + plc.directory URL). Public API: `resolve_handle(handle) -> Result<String, PdsClientError>` (DNS TXT `_atproto.{handle}` with HTTP `/.well-known/atproto-did` fallback), `discover_pds(did) -> Result<(String, PlcDidDocument), PdsClientError>` (fetches DID doc from plc.directory, extracts `atproto_pds` endpoint, verifies reachability via HEAD), `discover_auth_server(pds_url) -> Result<AuthServerMetadata, PdsClientError>` (fetches `/.well-known/oauth-authorization-server`, validates `code` response type + S256 challenge method), `pds_par(metadata, pkce_challenge, state, dpop_proof, dpop_jkt, login_hint?) -> Result<PdsParResponse, PdsClientError>` (PAR to arbitrary PDS), `pds_token_exchange(metadata, code, pkce_verifier, dpop_proof) -> Result<reqwest::Response, PdsClientError>` (returns raw response for caller nonce-retry), `build_pds_authorize_url(metadata, request_uri, login_hint?) -> String` (constructs browser redirect URL). Module-level XRPC functions (take `&OAuthClient`): `request_plc_operation_signature(client)`, `sign_plc_operation(client, request)`, `get_recommended_did_credentials(client)`. Types: `PlcDidDocument`, `PlcService`, `AuthServerMetadata`, `PdsParResponse`, `SignPlcOperationRequest`, `SignPlcOperationResponse`, `RecommendedCredentials`. `PdsClientError` enum: HANDLE_NOT_FOUND, DID_NOT_FOUND, PDS_UNREACHABLE, NETWORK_ERROR, INVALID_RESPONSE, OAUTH_FAILED (serialized as `{ code: "SCREAMING_SNAKE_CASE" }`) 45 + - `src/pds_client.rs` — PDS discovery and OAuth module for arbitrary PDS endpoints (not just our relay). `PdsClient` struct (stateless, wraps a `reqwest::Client` + plc.directory URL). Public API: `resolve_handle(handle) -> Result<String, PdsClientError>` (DNS TXT `_atproto.{handle}` with HTTP `/.well-known/atproto-did` fallback), `discover_pds(did) -> Result<(String, PlcDidDocument), PdsClientError>` (fetches DID doc from plc.directory, extracts `atproto_pds` endpoint, verifies reachability via HEAD), `discover_auth_server(pds_url) -> Result<AuthServerMetadata, PdsClientError>` (fetches `/.well-known/oauth-authorization-server`, validates `code` response type + S256 challenge method), `pds_par(metadata, pkce_challenge, state, dpop_proof, dpop_jkt, login_hint?) -> Result<PdsParResponse, PdsClientError>` (PAR to arbitrary PDS), `pds_token_exchange(metadata, code, pkce_verifier, dpop_proof) -> Result<reqwest::Response, PdsClientError>` (returns raw response for caller nonce-retry), `build_pds_authorize_url(metadata, request_uri, login_hint?) -> String` (constructs browser redirect URL), `fetch_audit_log(did) -> Result<String, PdsClientError>` (fetches PLC operation audit log as raw JSON from `{plc_directory_url}/{did}/log/audit`), `post_plc_operation(did, operation) -> Result<(), PdsClientError>` (POSTs signed PLC operation JSON to `{plc_directory_url}/{did}`). Module-level XRPC functions (take `&OAuthClient`): `request_plc_operation_signature(client)`, `sign_plc_operation(client, request)`, `get_recommended_did_credentials(client)`. Types: `PlcDidDocument` (Clone), `PlcService` (Clone), `AuthServerMetadata`, `PdsParResponse`, `SignPlcOperationRequest`, `SignPlcOperationResponse`, `RecommendedCredentials`. `PdsClientError` enum: HANDLE_NOT_FOUND, DID_NOT_FOUND, PDS_UNREACHABLE, NETWORK_ERROR, INVALID_RESPONSE, OAUTH_FAILED (serialized as `{ code: "SCREAMING_SNAKE_CASE" }`) 45 46 - `src/lib.rs::get_relay_url() -> Option<String>` — Tauri IPC command: loads relay base URL from Keychain, returns Some(url) if configured or None for first-launch 46 47 - `src/lib.rs::save_relay_url(url: String) -> Result<(), RelayConfigError>` — Tauri IPC command: validates URL format, pings `/xrpc/_health` on the relay, saves to Keychain, initializes `AppState.relay_client` (runtime configuration) 47 48 ··· 71 72 - `PdsClientError` variants serialize as `{ code: "SCREAMING_SNAKE_CASE" }` matching the `CreateAccountError` pattern; the `PdsUnreachable` variant's `reason` field is `#[serde(skip)]` (not sent to frontend) 72 73 - `PdsClient` is stateless (wraps `reqwest::Client` for connection pooling); default constructor targets `https://plc.directory`; test constructor accepts a custom URL for mock servers 73 74 - `PdsClient` is initialized eagerly in `AppState::new()` (not OnceLock) because it is cheap and stateless 75 + - `ClaimState` in `AppState` uses `tokio::sync::Mutex` (not `std::sync::Mutex`) because claim commands hold the lock across `.await` points; initialized to `None` in `AppState::new()` 76 + - `claim::resolve_identity` stores `ClaimState` in `AppState` for use by subsequent claim commands; calling it again resets the claim flow 77 + - `claim::start_pds_auth` reuses the existing deep-link callback mechanism (`pending_auth` oneshot channel) and emits `"pds_auth_ready"` event to the frontend on success 78 + - `claim::sign_and_verify_claim` performs 4-point local verification: (1) rotationKeys[0] is the device key, (2) `prev` CID chains correctly against the audit log, (3) no unexpected key additions or removals, (4) no unexpected service mutations 79 + - `claim::submit_claim` clears `ClaimState` on success (not on failure, allowing retries); validates caller DID matches `ClaimState.did` (defense-in-depth) 80 + - `claim::submit_claim` persists identity via `IdentityStore`: registers DID, ensures device key, stores re-fetched DID document and PLC audit log; tolerates `IdentityAlreadyExists` from prior partial claims 81 + - `ResolveError` variants serialize as `{ code: "SCREAMING_SNAKE_CASE" }` matching the existing error pattern 82 + - `ClaimError` variants serialize as `{ code: "SCREAMING_SNAKE_CASE" }` matching the existing error pattern; both use `thiserror::Error` for Display impls 74 83 - `pds_token_exchange` returns the raw `reqwest::Response` (not parsed) so callers can inspect `use_dpop_nonce` headers and implement retry logic 75 84 - XRPC identity functions (`request_plc_operation_signature`, `sign_plc_operation`, `get_recommended_did_credentials`) are module-level functions (not methods on `PdsClient`) because they require a DPoP-authenticated `OAuthClient` rather than the stateless HTTP client 76 85 - `resolve_handle` tries DNS TXT first (`_atproto.{handle}`), then HTTP `/.well-known/atproto-did`; returns `HANDLE_NOT_FOUND` only when both methods fail ··· 103 112 - Rust backend -> `tauri-plugin-deep-link` (registers `dev.malpercio.identitywallet:` URL scheme for OAuth callback) 104 113 - Rust backend -> `tauri-plugin-opener` (opens Safari for OAuth authorization) 105 114 - Rust backend -> plc.directory (via `reqwest` HTTP at runtime; used by `PdsClient::discover_pds` to fetch DID documents) 106 - - Rust backend -> arbitrary PDS endpoints (via `reqwest` HTTP at runtime; used by `PdsClient` for OAuth discovery, PAR, token exchange, and XRPC identity methods) 115 + - Rust backend -> arbitrary PDS endpoints (via `reqwest` HTTP at runtime; used by `PdsClient` for OAuth discovery, PAR, token exchange, XRPC identity methods, and claim flow PDS authentication) 116 + - Rust backend -> plc.directory `GET /{did}/log/audit` endpoint (via `PdsClient::fetch_audit_log`; fetches PLC operation audit log for signature verification during claim flow) 117 + - Rust backend -> plc.directory `POST /{did}` endpoint (via `PdsClient::post_plc_operation`; submits signed PLC operations during claim flow) 107 118 - Rust backend -> `hickory-resolver` (workspace dep: DNS TXT resolution for ATProto handle verification in `pds_client::try_resolve_dns`) 108 119 - Rust backend -> `urlencoding` (workspace dep: URL-encoding for OAuth authorize URL construction in `PdsClient::build_pds_authorize_url`) 109 120 - Rust backend -> iOS Keychain (via `security-framework` crate with `OSX_10_12` feature for SE access control APIs) ··· 232 243 - **PDS client separate from relay client (`pds_client.rs`)**: `PdsClient` handles discovery and OAuth against arbitrary PDS endpoints (not just our relay), while `RelayClient` (in `http.rs`) handles communication with the user's configured relay. The separation exists because PDS discovery targets endpoints the wallet learns at runtime (plc.directory, user's PDS), whereas `RelayClient` targets a single configured relay. `PdsClient` is stateless and uses `reqwest::Client` directly; `RelayClient` holds a runtime-configured base URL. 233 244 - **XRPC identity functions as module-level functions**: `request_plc_operation_signature`, `sign_plc_operation`, and `get_recommended_did_credentials` are standalone functions in `pds_client.rs` (not methods on `PdsClient`) because they require a DPoP-authenticated `OAuthClient` for the Authorization header, which `PdsClient`'s plain HTTP client cannot provide. This keeps `PdsClient` focused on unauthenticated discovery while XRPC calls use the existing `OAuthClient` infrastructure. 234 245 - **DNS resolution via hickory-resolver**: Handle resolution uses `hickory-resolver` for DNS TXT lookups (`_atproto.{handle}`), matching the same DNS library used by the relay crate (`crates/relay/src/dns.rs`). Falls back to HTTP `/.well-known/atproto-did` when DNS fails. 246 + - **Claim flow as multi-step state machine (`claim.rs`)**: The 5 claim commands form a sequential pipeline: `resolve_identity` -> `start_pds_auth` -> `request_claim_verification` -> `sign_and_verify_claim` -> `submit_claim`. State is persisted in `AppState.claim_state` (tokio::sync::Mutex) across commands. Each command validates prerequisites (e.g. `start_pds_auth` requires `ClaimState` to exist, `request_claim_verification` requires `pds_oauth_client`). The `_impl` test helpers extract core logic away from Tauri's `State` wrapper. 247 + - **PDS OAuth reuses deep-link mechanism**: `start_pds_auth` reuses the same `pending_auth` oneshot channel and `handle_deep_link` callback as `start_oauth_flow` in `oauth.rs`, so both relay OAuth and PDS OAuth share a single deep-link handler. Only one OAuth flow can be in progress at a time. 248 + - **PlcDidDocument and PlcService derive Clone**: Added to support cloning claim state data out of the tokio Mutex before releasing the lock for network calls. This pattern avoids holding the Mutex across `.await` points. 235 249 236 250 ## Invariants 237 251 ··· 255 269 - Keychain account `"oauth-refresh-token"` stores the OAuth refresh token; written alongside the access token 256 270 - `OAuthError` variant names serialize as SCREAMING_SNAKE_CASE to the frontend -- the TypeScript `OAuthError.code` union must match exactly 257 271 - `PdsClientError` variant names serialize as SCREAMING_SNAKE_CASE to the frontend -- the TypeScript `PdsClientError.code` union must match exactly (HANDLE_NOT_FOUND, DID_NOT_FOUND, PDS_UNREACHABLE, NETWORK_ERROR, INVALID_RESPONSE, OAUTH_FAILED) 272 + - `ResolveError` variant names serialize as SCREAMING_SNAKE_CASE to the frontend -- the TypeScript `ResolveError` union must match exactly (HANDLE_NOT_FOUND, DID_NOT_FOUND, PDS_UNREACHABLE, NETWORK_ERROR) 273 + - `ClaimError` variant names serialize as SCREAMING_SNAKE_CASE to the frontend -- the TypeScript `ClaimError` union must match exactly (INVALID_TOKEN, VERIFICATION_FAILED, PLC_DIRECTORY_ERROR, UNAUTHORIZED, NETWORK_ERROR) 258 274 - OAuth client_id is always `"dev.malpercio.identitywallet"` -- must match the seeded row in relay migration V013 and the `tauri.conf.json` bundle identifier 259 275 - OAuth redirect_uri is always `"dev.malpercio.identitywallet:/oauth/callback"` -- must match the deep-link scheme in `tauri.conf.json` and the seeded client_metadata redirect_uris in V013 260 276 - `DevicePublicKey` serializes with `#[serde(rename_all = "camelCase")]` -- TypeScript receives `{ multibase, keyId }` (not `key_id`) ··· 268 284 ## Key Files 269 285 270 286 - `src-tauri/tauri.conf.json` -- Tauri config: bundle ID, devUrl, frontendDist, window settings 271 - - `src-tauri/src/lib.rs` -- Tauri IPC commands (`get_relay_url`, `save_relay_url`, `create_account`, `get_or_create_device_key`, `sign_with_device_key`, `perform_did_ceremony`, `start_oauth_flow`, `home::load_home_data`, `home::log_out`), `run()` (mobile entry point), deep-link plugin setup, startup token restore 287 + - `src-tauri/src/lib.rs` -- Tauri IPC commands (`get_relay_url`, `save_relay_url`, `create_account`, `get_or_create_device_key`, `sign_with_device_key`, `perform_did_ceremony`, `start_oauth_flow`, `home::load_home_data`, `home::log_out`, `claim::resolve_identity`, `claim::start_pds_auth`, `claim::request_claim_verification`, `claim::sign_and_verify_claim`, `claim::submit_claim`), `run()` (mobile entry point), deep-link plugin setup, startup token restore 272 288 - `src-tauri/src/home.rs` -- Home screen Tauri commands: `load_home_data` (concurrent relay health + getSession), `log_out` (Keychain wipe + session clear); output types: HomeData, SessionInfo 273 289 - `src-tauri/src/device_key.rs` -- P-256 device key module: `#[cfg]`-dispatched `get_or_create()` and `sign()` (simulator software path vs. Secure Enclave) 274 290 - `src-tauri/src/identity_store.rs` -- Multi-identity Keychain management: IdentityStore (add/remove/list identities, per-DID device key generation, DID doc + PLC log persistence) 275 - - `src-tauri/src/pds_client.rs` -- PDS discovery and OAuth to arbitrary PDS: PdsClient (resolve_handle, discover_pds, discover_auth_server, pds_par, pds_token_exchange, build_pds_authorize_url); XRPC identity functions (request_plc_operation_signature, sign_plc_operation, get_recommended_did_credentials) 291 + - `src-tauri/src/claim.rs` -- PLC rotation key claim flow: 5 Tauri IPC commands (resolve_identity, start_pds_auth, request_claim_verification, sign_and_verify_claim, submit_claim); types (IdentityInfo, VerifiedClaimOp, OpDiff, ServiceChange, ClaimResult, ClaimState); error enums (ResolveError, ClaimError) 292 + - `src-tauri/src/pds_client.rs` -- PDS discovery and OAuth to arbitrary PDS: PdsClient (resolve_handle, discover_pds, discover_auth_server, pds_par, pds_token_exchange, build_pds_authorize_url, fetch_audit_log, post_plc_operation); XRPC identity functions (request_plc_operation_signature, sign_plc_operation, get_recommended_did_credentials) 276 293 - `src-tauri/src/main.rs` -- Desktop entry point (calls `lib::run()`) 277 294 - `src-tauri/src/oauth.rs` -- OAuth PKCE module: AppState, DPoPKeypair, OAuthSession, PKCE utilities, start_oauth_flow command, handle_deep_link 278 295 - `src-tauri/src/oauth_client.rs` -- OAuthClient: authenticated HTTP client with DPoP proofs and lazy token refresh 279 296 - `src-tauri/src/keychain.rs` -- iOS Keychain abstraction (store_item, get_item, delete_item); Relay URL helpers (store_relay_url, load_relay_url); OAuth helpers (store_dpop_key, load_dpop_key, store_oauth_tokens, load_oauth_tokens) 280 297 - `src-tauri/src/http.rs` -- RelayClient with runtime-configurable base URL; OAuth methods (par, token_exchange) 281 298 - `src-tauri/.cargo/config.toml` -- Cargo toolchain overrides for iOS cross-compilation (CC, AR, linker per target) 282 - - `src/lib/ipc.ts` -- Typed TypeScript wrappers for all Tauri IPC commands (getRelayUrl, saveRelayUrl, createAccount, getOrCreateDeviceKey, signWithDeviceKey, performDIDCeremony, startOAuthFlow, loadHomeData, logOut) 299 + - `src/lib/ipc.ts` -- Typed TypeScript wrappers for all Tauri IPC commands (getRelayUrl, saveRelayUrl, createAccount, getOrCreateDeviceKey, signWithDeviceKey, performDIDCeremony, startOAuthFlow, loadHomeData, logOut, resolveIdentity, startPdsAuth, requestClaimVerification, signAndVerifyClaim, submitClaim) 283 300 - `src/lib/components/onboarding/` -- Eleven onboarding screen components (RelayConfigScreen, WelcomeScreen, ClaimCodeScreen, EmailScreen, HandleScreen, PasswordScreen, LoadingScreen, DIDCeremonyScreen, DIDSuccessScreen, ShamirBackupScreen, AuthenticatingScreen) 284 301 - `src/lib/components/home/` -- Three home screen components (HomeScreen, DIDDocumentScreen, RecoveryInfoScreen) plus DIDAvatar utility component 285 302 - `src/routes/+page.svelte` -- 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)