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 project context for recovery override implementation

Complete the CLAUDE.md updates that were partially done in Phase 4:
- Add recovery_override to +page.svelte navigation flow description
- Document AlertDetailScreen's new onoverride callback prop
- Document RecoveryOverrideScreen component props and behavior
- Add PdsClient accessor methods (plc_directory_url, client) to API docs
- Add recovery_state to AppState field list
- Add chrono workspace dependency and plc.directory GET /{did} endpoint
- Update plc.directory endpoint descriptions to include recovery flows
- Bump freshness dates to 2026-03-31

+12 -9
+1 -1
CLAUDE.md
··· 1 1 # ezpds 2 2 3 - Last verified: 2026-03-27 3 + Last verified: 2026-03-31 4 4 5 5 ## Tech Stack 6 6 - Language: Rust (stable channel via rust-toolchain.toml)
+11 -8
apps/identity-wallet/CLAUDE.md
··· 1 1 # Identity Wallet Mobile App 2 2 3 - Last verified: 2026-03-30 4 - Last updated: 2026-03-30 3 + Last verified: 2026-03-31 4 + Last updated: 2026-03-31 5 5 6 6 ## Purpose 7 7 ··· 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 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 - - `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 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 -> recovery_override. 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 21 21 **Guarantees:** 22 22 - SSR is disabled globally (`ssr = false` in `src/routes/+layout.ts`); the frontend is a fully static SPA loaded from disk by WKWebView ··· 36 36 - `src/lib.rs::sign_with_device_key(data: Vec<u8>) -> Result<Vec<u8>, DeviceKeyError>` — Tauri IPC command: delegates to `device_key::sign()` 37 37 - `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 38 38 - `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 } 39 - - `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/oauth.rs` — OAuth PKCE client module: `AppState` (pending_auth + oauth_session mutexes + relay_client OnceLock + pds_client + claim_state + recovery_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 40 40 - `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(did, 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) 41 41 - `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)` 42 42 - `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>` 43 43 - `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) 44 44 - `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` 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 - - `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/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: `plc_directory_url() -> &str` (returns the plc.directory base URL), `client() -> &Client` (returns the inner reqwest client), `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 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) 49 49 - `src/lib.rs::check_identity_status() -> Result<Vec<IdentityStatus>, MonitorError>` — Tauri IPC command (delegates to `PlcMonitor::check_all`) ··· 88 88 - `MonitorError` variants serialize as `{ code: "SCREAMING_SNAKE_CASE" }` matching the existing error pattern 89 89 - `UnauthorizedChange.created_at` is the ISO 8601 timestamp from plc.directory's audit log; the frontend computes the 72-hour recovery deadline from this value 90 90 - `IdentityListHome` accepts an optional `onalert` callback prop `(did: string, changes: UnauthorizedChange[]) => void`; identity cards display an alert badge when `alertData` has entries for that DID 91 - - `AlertDetailScreen` accepts `did`, `changes: UnauthorizedChange[]`, and `onback` callback props; displays each unauthorized change with signing key, recovery deadline, and urgency coloring; updates countdown every 60 seconds via `setInterval` 91 + - `AlertDetailScreen` accepts `did`, `changes: UnauthorizedChange[]`, `onback` callback, and `onoverride: (cid: string, createdAt: string) => void` callback props; displays each unauthorized change with signing key, recovery deadline, and urgency coloring; "Review & Override" button calls `onoverride` (disabled when urgency is `'expired'`); updates countdown every 60 seconds via `setInterval` 92 + - `RecoveryOverrideScreen` accepts `did`, `operationCid`, `createdAt`, `onback`, and `onsuccess` callback props; calls `buildRecoveryOverride()` on mount, displays the signed operation diff for user review, then calls `submitRecoveryOverride()` on confirmation; shows recovery deadline countdown and error states 92 93 - `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 93 94 - `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 94 95 - `claim::submit_claim` clears `ClaimState` on success (not on failure, allowing retries); validates caller DID matches `ClaimState.did` (defense-in-depth) ··· 128 129 - Rust backend -> `tauri-plugin-opener` (opens Safari for OAuth authorization) 129 130 - Rust backend -> plc.directory (via `reqwest` HTTP at runtime; used by `PdsClient::discover_pds` to fetch DID documents) 130 131 - 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) 131 - - 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) 132 - - Rust backend -> plc.directory `POST /{did}` endpoint (via `PdsClient::post_plc_operation`; submits signed PLC operations during claim flow) 132 + - Rust backend -> plc.directory `GET /{did}/log/audit` endpoint (via `PdsClient::fetch_audit_log`; fetches PLC operation audit log for signature verification during claim and recovery flows) 133 + - Rust backend -> plc.directory `POST /{did}` endpoint (via `PdsClient::post_plc_operation`; submits signed PLC operations during claim and recovery flows) 134 + - Rust backend -> plc.directory `GET /{did}` endpoint (via `PdsClient::client()` direct HTTP; fetches DID document after recovery override to update cache) 133 135 - Rust backend -> `hickory-resolver` (workspace dep: DNS TXT resolution for ATProto handle verification in `pds_client::try_resolve_dns`) 134 136 - Rust backend -> `urlencoding` (workspace dep: URL-encoding for OAuth authorize URL construction in `PdsClient::build_pds_authorize_url`) 137 + - Rust backend -> `chrono` (workspace dep: date/time parsing for recovery window computation in `recovery.rs`) 135 138 - Rust backend -> iOS Keychain (via `security-framework` crate with `OSX_10_12` feature for SE access control APIs) 136 139 - Rust backend -> Secure Enclave hardware (real iOS device only; via `security-framework` `SecKey`/`GenerateKeyOptions`/`Token::SecureEnclave`) 137 140 - `src-tauri/gen/` -> NOT tracked in git; generated per-developer by `cargo tauri ios init` (gitignored)