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 import flow screens and multi-identity home

Reflect the new frontend contracts added during the mobile claim flow
frontend phase: 3 new Tauri IPC commands (list_identities,
get_stored_did_doc, get_device_key_id), 7 new screen components
(ModeSelectScreen, IdentityInputScreen, PdsAuthScreen,
EmailVerificationScreen, ReviewOperationScreen, ClaimSuccessScreen,
IdentityListHome), restructured state machine with mode_select entry
point and dual-flow routing, and identity-aware onMount logic.

authored by

Malpercio and committed by
Tangled
f45ba15c 4c413f14

+16 -9
+16 -9
apps/identity-wallet/CLAUDE.md
··· 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()`, `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 - - `src/lib/components/onboarding/` — twelve onboarding screen components (RelayConfigScreen, WelcomeScreen, ClaimCodeScreen, EmailScreen, HandleScreen, PasswordScreen, LoadingScreen, DIDCeremonyScreen, DIDSuccessScreen, ShamirBackupScreen, HandleRegistrationScreen, AuthenticatingScreen) 17 - - `src/lib/components/home/` — three home screen components (HomeScreen, DIDDocumentScreen, RecoveryInfoScreen) plus DIDAvatar utility component (deterministic DID-derived hue circle) 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) 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()`, `listIdentities()`, `getStoredDidDoc()`, `getDeviceKeyId()`, and their associated types (`DevicePublicKey`, `DeviceKeyError`, `CreateAccountResult`, `CreateAccountError`, `DIDCeremonyResult`, `DIDCeremonyError`, `OAuthError`, `SessionInfo`, `HomeData`, `RelayConfigError`, `IdentityInfo`, `VerifiedClaimOp`, `OpDiff`, `ServiceChange`, `ClaimResult`, `ResolveError`, `ClaimError`, `IdentityStoreError`) 16 + - `src/lib/components/onboarding/` — eighteen onboarding screen components (ModeSelectScreen, RelayConfigScreen, WelcomeScreen, ClaimCodeScreen, EmailScreen, HandleScreen, PasswordScreen, LoadingScreen, DIDCeremonyScreen, DIDSuccessScreen, ShamirBackupScreen, HandleRegistrationScreen, AuthenticatingScreen, IdentityInputScreen, PdsAuthScreen, EmailVerificationScreen, ReviewOperationScreen, ClaimSuccessScreen) 17 + - `src/lib/components/home/` — four home screen components (IdentityListHome, HomeScreen, DIDDocumentScreen, RecoveryInfoScreen) plus DIDAvatar utility component (deterministic DID-derived hue circle) 18 + - `src/routes/+page.svelte` — root page: two-flow state machine starting at `mode_select`. **Create flow:** mode_select -> relay_config -> welcome -> claim_code -> email -> handle -> password -> loading -> did_ceremony -> did_success -> shamir_backup -> handle_registration -> complete -> authenticating -> home. **Import flow:** mode_select -> identity_input -> pds_auth -> email_verification -> review_operation -> claim_success -> home. **Home:** home -> identity_detail -> did_document / recovery_info. On mount, checks for existing identities via `listIdentities()` and skips to `home` if any exist 19 19 20 20 **Guarantees:** 21 21 - SSR is disabled globally (`ssr = false` in `src/routes/+layout.ts`); the frontend is a fully static SPA loaded from disk by WKWebView ··· 43 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` 44 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" }`) 45 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" }`) 46 + - `src/lib.rs::list_identities() -> Result<Vec<String>, IdentityStoreError>` — Tauri IPC command: returns managed DIDs from Keychain via `IdentityStore::list_identities()`; returns empty list if no identities claimed 47 + - `src/lib.rs::get_stored_did_doc(did: String) -> Result<Option<serde_json::Value>, IdentityStoreError>` — Tauri IPC command: retrieves stored DID document as parsed JSON for a claimed identity; returns None if not stored 48 + - `src/lib.rs::get_device_key_id(did: String) -> Result<String, IdentityStoreError>` — Tauri IPC command: returns the device key's did:key URI for a claimed identity via `IdentityStore::get_or_create_device_key()` 46 49 - `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 47 50 - `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) 48 51 ··· 246 249 - **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 250 - **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 251 - **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. 252 + - **Mode selector as entry point**: The app starts at `mode_select` (not `relay_config`), offering two paths: "Create new identity" (original onboarding flow) and "Import existing identity" (claim flow). On mount, `+page.svelte` calls `listIdentities()` and skips directly to `home` if any identities exist. This identity-aware routing replaces the previous relay-URL-based skip logic. 253 + - **IdentityListHome replaces HomeScreen at `home` step**: The `home` state now renders `IdentityListHome` (multi-identity card list) instead of the single-identity `HomeScreen`. `IdentityListHome` shows all managed identities with handle, PDS URL, and rotation key status badges. Tapping an identity navigates to `identity_detail` (renders `DIDDocumentScreen`). The original `HomeScreen` component still exists for legacy relay-authenticated sessions but is no longer wired into the state machine. 254 + - **Identity store IPC commands are synchronous**: `list_identities`, `get_stored_did_doc`, and `get_device_key_id` are non-async Tauri commands (no `async fn`, no `State<>` parameter) -- they call `IdentityStore` methods directly since Keychain access is synchronous. This differs from most other Tauri commands which are async and take `State<AppState>`. 249 255 250 256 ## Invariants 251 257 ··· 271 277 - `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 278 - `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 279 - `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) 280 + - `IdentityStoreError` variant names serialize as SCREAMING_SNAKE_CASE to the frontend -- the TypeScript `IdentityStoreError.code` union must match exactly (IDENTITY_NOT_FOUND, IDENTITY_ALREADY_EXISTS, KEYCHAIN_ERROR, KEY_GENERATION_FAILED, SERIALIZATION_ERROR) 274 281 - OAuth client_id is always `"dev.malpercio.identitywallet"` -- must match the seeded row in relay migration V013 and the `tauri.conf.json` bundle identifier 275 282 - 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 276 283 - `DevicePublicKey` serializes with `#[serde(rename_all = "camelCase")]` -- TypeScript receives `{ multibase, keyId }` (not `key_id`) ··· 284 291 ## Key Files 285 292 286 293 - `src-tauri/tauri.conf.json` -- Tauri config: bundle ID, devUrl, frontendDist, window settings 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 294 + - `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`, `list_identities`, `get_stored_did_doc`, `get_device_key_id`), `run()` (mobile entry point), deep-link plugin setup, startup token restore 288 295 - `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 289 296 - `src-tauri/src/device_key.rs` -- P-256 device key module: `#[cfg]`-dispatched `get_or_create()` and `sign()` (simulator software path vs. Secure Enclave) 290 297 - `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) ··· 296 303 - `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) 297 304 - `src-tauri/src/http.rs` -- RelayClient with runtime-configurable base URL; OAuth methods (par, token_exchange) 298 305 - `src-tauri/.cargo/config.toml` -- Cargo toolchain overrides for iOS cross-compilation (CC, AR, linker per target) 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) 300 - - `src/lib/components/onboarding/` -- Eleven onboarding screen components (RelayConfigScreen, WelcomeScreen, ClaimCodeScreen, EmailScreen, HandleScreen, PasswordScreen, LoadingScreen, DIDCeremonyScreen, DIDSuccessScreen, ShamirBackupScreen, AuthenticatingScreen) 301 - - `src/lib/components/home/` -- Three home screen components (HomeScreen, DIDDocumentScreen, RecoveryInfoScreen) plus DIDAvatar utility component 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) 306 + - `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, listIdentities, getStoredDidDoc, getDeviceKeyId) 307 + - `src/lib/components/onboarding/` -- Eighteen onboarding screen components (ModeSelectScreen, RelayConfigScreen, WelcomeScreen, ClaimCodeScreen, EmailScreen, HandleScreen, PasswordScreen, LoadingScreen, DIDCeremonyScreen, DIDSuccessScreen, ShamirBackupScreen, HandleRegistrationScreen, AuthenticatingScreen, IdentityInputScreen, PdsAuthScreen, EmailVerificationScreen, ReviewOperationScreen, ClaimSuccessScreen) 308 + - `src/lib/components/home/` -- Four home screen components (IdentityListHome, HomeScreen, DIDDocumentScreen, RecoveryInfoScreen) plus DIDAvatar utility component 309 + - `src/routes/+page.svelte` -- Two-flow state machine starting at mode_select; Create flow: mode_select -> relay_config -> ... -> home; Import flow: mode_select -> identity_input -> pds_auth -> email_verification -> review_operation -> claim_success -> home; Home: home (IdentityListHome) -> identity_detail -> did_document / recovery_info 303 310 - `src/routes/+layout.ts` -- `ssr = false; prerender = false` (global SPA config) 304 311 - `svelte.config.js` -- adapter-static with `pages: 'dist'` (SPA mode, matches tauri.conf.json) 305 312 - `vite.config.ts` -- Tauri-compatible Vite server (clearScreen, HMR via TAURI_DEV_HOST, envPrefix)