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(identity-wallet): document recovery override module in CLAUDE.md

Add comprehensive documentation for the recovery override feature:

1. Under **Contracts → Rust Backend → Exposes**, add src/recovery.rs module
documentation with build_recovery_override and submit_recovery_override
functions, Tauri IPC commands, types, and error variants.

2. Under **Contracts → Frontend → Exposes**, update src/lib/ipc.ts exports
to include buildRecoveryOverride, submitRecoveryOverride, SignedRecoveryOp,
and RecoveryError types.

3. Update the home components list to include the new RecoveryOverrideScreen
component.

4. Add recovery module details to Key Files section.

5. Add invariants for recovery error serialization, SignedRecoveryOp camelCase
serialization, 72-hour recovery window, and RecoveryState mutex pattern.

6. Update +page.svelte state machine path to include recovery_override step.

+12 -6
+12 -6
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()`, `listIdentities()`, `getStoredDidDoc()`, `getDeviceKeyId()`, `checkIdentityStatus()`, and their associated types (`DevicePublicKey`, `DeviceKeyError`, `CreateAccountResult`, `CreateAccountError`, `DIDCeremonyResult`, `DIDCeremonyError`, `OAuthError`, `SessionInfo`, `HomeData`, `RelayConfigError`, `IdentityInfo`, `VerifiedClaimOp`, `OpDiff`, `ServiceChange`, `ClaimResult`, `ResolveError`, `ClaimError`, `IdentityStoreError`, `UnauthorizedChange`, `IdentityStatus`) 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()`, `checkIdentityStatus()`, `buildRecoveryOverride()`, `submitRecoveryOverride()`, and their associated types (`DevicePublicKey`, `DeviceKeyError`, `CreateAccountResult`, `CreateAccountError`, `DIDCeremonyResult`, `DIDCeremonyError`, `OAuthError`, `SessionInfo`, `HomeData`, `RelayConfigError`, `IdentityInfo`, `VerifiedClaimOp`, `OpDiff`, `ServiceChange`, `ClaimResult`, `ResolveError`, `ClaimError`, `IdentityStoreError`, `UnauthorizedChange`, `IdentityStatus`, `SignedRecoveryOp`, `RecoveryError`) 16 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/` — five home screen components (IdentityListHome, HomeScreen, DIDDocumentScreen, RecoveryInfoScreen, AlertDetailScreen) plus DIDAvatar utility component (deterministic DID-derived hue circle) 17 + - `src/lib/components/home/` — six home screen components (IdentityListHome, HomeScreen, DIDDocumentScreen, RecoveryInfoScreen, AlertDetailScreen, RecoveryOverrideScreen) plus DIDAvatar utility component (deterministic DID-derived hue circle) 18 18 - `src/lib/utils/deadline.ts` — PLC recovery deadline utilities: `getDeadline(createdAt)` (adds 72h to ISO 8601 timestamp), `getUrgency(deadline)` (returns `'safe'` | `'warning'` | `'critical'` | `'expired'`), `formatCountdown(deadline)` (human-readable `"Xh Ym remaining"`). `Urgency` type exported. Thresholds: expired = 0, critical < 4h, warning < 24h, safe >= 24h 19 19 - `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 / alert_detail. On mount, checks for existing identities via `listIdentities()` and skips to `home` if any exist. Registers a `visibilitychange` listener that calls `checkIdentityStatus()` when the app returns to foreground while on the `home` step 20 20 ··· 45 45 - `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" }`) 46 46 - `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" }`) 47 47 - `src/plc_monitor.rs` — PLC monitoring module: `PlcMonitor` (borrows `PdsClient`; `check_all()` iterates all managed DIDs, `check_for_changes(did)` diffs current audit log against cached log and classifies new entries as authorized/unauthorized by verifying signatures against the device key); `run_monitoring_loop(app_handle)` (spawned once at app startup, checks every 15 minutes via `tokio::time::interval` with `MissedTickBehavior::Delay`, emits `"plc_alert"` Tauri event to frontend when unauthorized changes detected); `check_identity_status` (Tauri IPC command: synchronous foreground check of all managed identities, returns `Vec<IdentityStatus>`). Types: `UnauthorizedChange` { cid, created_at, signing_key, operation } (camelCase serialization), `IdentityStatus` { did, alert_count, unauthorized_changes } (camelCase serialization), `MonitorError` { NetworkError, IdentityStoreError, ParseError } (SCREAMING_SNAKE_CASE tag serialization) 48 + - `src/recovery.rs` — Recovery override module: `build_recovery_override(pds_client, did, unauthorized_op_cid) -> Result<SignedRecoveryOp, RecoveryError>` (fetches audit log, identifies fork point, builds counter-operation restoring pre-unauthorized state, signs with per-DID device key), `submit_recovery_override(pds_client, did, signed_op) -> Result<ClaimResult, RecoveryError>` (POSTs to plc.directory, updates cached log and DID doc); Tauri IPC commands: `build_recovery_override_cmd`, `submit_recovery_override_cmd`. Types: `SignedRecoveryOp` { diff, signed_op }, `RecoveryState` { did, signed_op }, `RecoveryError` (RECOVERY_WINDOW_EXPIRED, SIGNING_FAILED, PLC_DIRECTORY_ERROR, NETWORK_ERROR, IDENTITY_NOT_FOUND, UNAUTHORIZED_CHANGE_NOT_FOUND) 48 49 - `src/lib.rs::check_identity_status() -> Result<Vec<IdentityStatus>, MonitorError>` — Tauri IPC command (delegates to `PlcMonitor::check_all`) 49 50 - `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 50 51 - `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 ··· 310 311 - Keychain account `"managed-dids"` stores a JSON array of all managed DID strings (e.g. `["did:plc:abc","did:plc:def"]`); the single source of truth for which identities are registered in `IdentityStore` 311 312 - Per-DID Keychain accounts follow the `"{did}:suffix"` pattern with six suffixes: `device-key` (P-256 private key scalar, software path only; not written on SE path), `device-key-pub` (compressed public key, SE path only), `device-key-app-label` (SE application_label, SE path only), `did-doc` (opaque DID document JSON), `plc-log` (opaque PLC audit log JSON), `oauth-tokens` (reserved for per-DID OAuth tokens) 312 313 - `IdentityStore` P-256 multicodec prefix `[0x80, 0x24]` is duplicated from `crates/crypto/src/keys.rs` (same rationale as `device_key.rs` -- `pub(crate)` constant cannot be imported cross-crate) 314 + - `RecoveryError` variant names serialize as SCREAMING_SNAKE_CASE to the frontend -- the TypeScript `RecoveryError` union in `ipc.ts` must match exactly 315 + - `SignedRecoveryOp` serializes with `#[serde(rename_all = "camelCase")]` -- TypeScript receives `{ diff, signedOp }` 316 + - Recovery window is 72 hours from the unauthorized operation's `created_at` timestamp; computed locally but enforced by plc.directory 317 + - `RecoveryState` in `AppState` uses `tokio::sync::Mutex` (same as `ClaimState`) because recovery commands hold the lock across `.await` points 313 318 314 319 ## Key Files 315 320 316 321 - `src-tauri/tauri.conf.json` -- Tauri config: bundle ID, devUrl, frontendDist, window settings 317 - - `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`, `plc_monitor::check_identity_status`), `run()` (mobile entry point), deep-link plugin setup, startup token restore, PLC monitoring loop spawn 322 + - `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`, `plc_monitor::check_identity_status`, `recovery::build_recovery_override_cmd`, `recovery::submit_recovery_override_cmd`), `run()` (mobile entry point), deep-link plugin setup, startup token restore, PLC monitoring loop spawn 318 323 - `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 319 324 - `src-tauri/src/device_key.rs` -- P-256 device key module: `#[cfg]`-dispatched `get_or_create()` and `sign()` (simulator software path vs. Secure Enclave) 320 325 - `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) 321 326 - `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) 322 327 - `src-tauri/src/plc_monitor.rs` -- PLC monitoring: PlcMonitor (check_all, check_for_changes), run_monitoring_loop (15-min background timer), check_identity_status (IPC command); types (UnauthorizedChange, IdentityStatus, MonitorError) 328 + - `src-tauri/src/recovery.rs` -- Recovery override: build_recovery_override_cmd, submit_recovery_override_cmd; fork-point identification, per-DID signing, recovery window check 323 329 - `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) 324 330 - `src-tauri/src/main.rs` -- Desktop entry point (calls `lib::run()`) 325 331 - `src-tauri/src/oauth.rs` -- OAuth PKCE module: AppState, DPoPKeypair, OAuthSession, PKCE utilities, start_oauth_flow command, handle_deep_link ··· 327 333 - `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) 328 334 - `src-tauri/src/http.rs` -- RelayClient with runtime-configurable base URL; OAuth methods (par, token_exchange) 329 335 - `src-tauri/.cargo/config.toml` -- Cargo toolchain overrides for iOS cross-compilation (CC, AR, linker per target) 330 - - `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, checkIdentityStatus) 336 + - `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, checkIdentityStatus, buildRecoveryOverride, submitRecoveryOverride) 331 337 - `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) 332 - - `src/lib/components/home/` -- Five home screen components (IdentityListHome, HomeScreen, DIDDocumentScreen, RecoveryInfoScreen, AlertDetailScreen) plus DIDAvatar utility component 338 + - `src/lib/components/home/` -- Six home screen components (IdentityListHome, HomeScreen, DIDDocumentScreen, RecoveryInfoScreen, AlertDetailScreen, RecoveryOverrideScreen) plus DIDAvatar utility component 333 339 - `src/lib/utils/deadline.ts` -- PLC recovery deadline utilities (getDeadline, getUrgency, formatCountdown); tested by `deadline.test.ts` 334 - - `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 / alert_detail; visibilitychange handler calls checkIdentityStatus() on foreground 340 + - `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 / alert_detail -> recovery_override; visibilitychange handler calls checkIdentityStatus() on foreground 335 341 - `src/routes/+layout.ts` -- `ssr = false; prerender = false` (global SPA config) 336 342 - `svelte.config.js` -- adapter-static with `pages: 'dist'` (SPA mode, matches tauri.conf.json) 337 343 - `vite.config.ts` -- Tauri-compatible Vite server (clearScreen, HMR via TAURI_DEV_HOST, envPrefix)