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 PLC monitoring and alerting

Add contracts, guarantees, invariants, key decisions, and key files for
the plc_monitor module, check_identity_status IPC command, "plc_alert"
Tauri event, AlertDetailScreen component, deadline utilities, vitest
setup, and the visibilitychange foreground-check handler.

+34 -9
+34 -9
apps/identity-wallet/CLAUDE.md
··· 1 1 # Identity Wallet Mobile App 2 2 3 - Last verified: 2026-03-29 4 - Last updated: 2026-03-29 3 + Last verified: 2026-03-30 4 + Last updated: 2026-03-30 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()`, `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`) 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`) 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/` — 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 17 + - `src/lib/components/home/` — five home screen components (IdentityListHome, HomeScreen, DIDDocumentScreen, RecoveryInfoScreen, AlertDetailScreen) plus DIDAvatar utility component (deterministic DID-derived hue circle) 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 20 20 21 **Guarantees:** 21 22 - SSR is disabled globally (`ssr = false` in `src/routes/+layout.ts`); the frontend is a fully static SPA loaded from disk by WKWebView ··· 43 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` 44 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" }`) 45 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 + - `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/lib.rs::check_identity_status() -> Result<Vec<IdentityStatus>, MonitorError>` — Tauri IPC command (delegates to `PlcMonitor::check_all`) 46 49 - `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 50 - `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 51 - `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()` ··· 77 80 - `PdsClient` is initialized eagerly in `AppState::new()` (not OnceLock) because it is cheap and stateless 78 81 - `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()` 79 82 - `claim::resolve_identity` stores `ClaimState` in `AppState` for use by subsequent claim commands; calling it again resets the claim flow 83 + - `plc_monitor::run_monitoring_loop` is spawned once during `setup()` in `lib.rs`; skips the first immediate tick (lets app finish initializing); uses `MissedTickBehavior::Delay` so iOS app suspension does not cause burst-fire of missed checks 84 + - `plc_monitor::check_for_changes` returns `Ok(vec![])` (not an error) when plc.directory is unreachable or the audit log cannot be parsed; errors are logged via `tracing::warn` and the monitor retries on the next cycle 85 + - `plc_monitor::check_for_changes` classifies new audit log entries by verifying signatures against the per-DID device key; entries signed by the device key are authorized (silently consumed); entries signed by any other key produce an `UnauthorizedChange` alert 86 + - `plc_monitor::check_for_changes` updates the cached PLC audit log in Keychain (via `IdentityStore::store_plc_log`) after processing; subsequent cycles only see entries newer than the cache 87 + - `MonitorError` variants serialize as `{ code: "SCREAMING_SNAKE_CASE" }` matching the existing error pattern 88 + - `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 89 + - `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 90 + - `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` 80 91 - `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 81 92 - `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 82 93 - `claim::submit_claim` clears `ClaimState` on success (not on failure, allowing retries); validates caller DID matches `ClaimState.did` (defense-in-depth) ··· 252 263 - **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 264 - **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 265 - **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>`. 266 + - **PLC monitoring: background timer + foreground check**: Two complementary mechanisms detect unauthorized PLC operations. (1) A background `tokio::time::interval` loop runs every 15 minutes, spawned once at app startup via `run_monitoring_loop`. (2) A `visibilitychange` listener in `+page.svelte` calls `checkIdentityStatus()` when the app returns to foreground. The timer uses `MissedTickBehavior::Delay` so iOS app suspension does not cause burst-fire of missed ticks. 267 + - **PlcMonitor borrows PdsClient**: `PlcMonitor` takes `&PdsClient` (not owned) because `PdsClient` is a shared singleton on `AppState`. The monitor is constructed fresh on each cycle from the managed `AppState` reference, avoiding lifetime issues with long-lived borrows across async boundaries. 268 + - **Graceful degradation on network errors**: `check_for_changes` returns `Ok(vec![])` when plc.directory is unreachable or audit log parsing fails, rather than surfacing errors to the UI. This prevents false alarms or error screens when the user has no network connectivity. Errors are logged via `tracing::warn` for diagnostics. 269 + - **Signing key identification**: When an unauthorized change is detected, `identify_signing_key` attempts to identify the signer by trying each rotation key from the previous operation in the audit log. If no key matches, `signing_key` is `None`. This is best-effort -- the signer may have used a key not in the previous rotation key set. 270 + - **Vitest for frontend unit tests**: `vitest` added as a dev dependency with `pnpm test` script (`vitest run`). Used for pure-logic utilities (e.g. `deadline.ts`) that do not require Tauri IPC mocking. 271 + - **AlertDetailScreen countdown**: `AlertDetailScreen` updates `now` via a 60-second `setInterval`, which re-computes urgency and countdown display. The timer is cleaned up in `onDestroy` to prevent leaks if the component is unmounted. 255 272 256 273 ## Invariants 257 274 ··· 278 295 - `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) 279 296 - `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 297 - `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) 298 + - `MonitorError` variant names serialize as SCREAMING_SNAKE_CASE to the frontend -- the TypeScript consumer (if any) must match exactly (NETWORK_ERROR, IDENTITY_STORE_ERROR, PARSE_ERROR) 299 + - `UnauthorizedChange` serializes with `#[serde(rename_all = "camelCase")]` -- TypeScript receives `{ cid, createdAt, signingKey, operation }`; the TypeScript `UnauthorizedChange` type in `ipc.ts` must match exactly 300 + - `IdentityStatus` serializes with `#[serde(rename_all = "camelCase")]` -- TypeScript receives `{ did, alertCount, unauthorizedChanges }`; the TypeScript `IdentityStatus` type in `ipc.ts` must match exactly 301 + - Tauri event `"plc_alert"` payload is `Vec<IdentityStatus>` (JSON array of identity statuses); the frontend `IdentityListHome` component listens for this event to update alert badges in real time 302 + - PLC monitoring interval is 15 minutes (`MONITOR_INTERVAL_SECS = 900`); changing this constant alters battery/network impact 303 + - Recovery deadline window is 72 hours (`RECOVERY_WINDOW_MS` in `deadline.ts`); this matches the PLC directory's 72-hour recovery window specification 281 304 - OAuth client_id is always `"dev.malpercio.identitywallet"` -- must match the seeded row in relay migration V013 and the `tauri.conf.json` bundle identifier 282 305 - 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 283 306 - `DevicePublicKey` serializes with `#[serde(rename_all = "camelCase")]` -- TypeScript receives `{ multibase, keyId }` (not `key_id`) ··· 291 314 ## Key Files 292 315 293 316 - `src-tauri/tauri.conf.json` -- Tauri config: bundle ID, devUrl, frontendDist, window settings 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 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 295 318 - `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 296 319 - `src-tauri/src/device_key.rs` -- P-256 device key module: `#[cfg]`-dispatched `get_or_create()` and `sign()` (simulator software path vs. Secure Enclave) 297 320 - `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) 298 321 - `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 + - `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) 299 323 - `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) 300 324 - `src-tauri/src/main.rs` -- Desktop entry point (calls `lib::run()`) 301 325 - `src-tauri/src/oauth.rs` -- OAuth PKCE module: AppState, DPoPKeypair, OAuthSession, PKCE utilities, start_oauth_flow command, handle_deep_link ··· 303 327 - `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) 304 328 - `src-tauri/src/http.rs` -- RelayClient with runtime-configurable base URL; OAuth methods (par, token_exchange) 305 329 - `src-tauri/.cargo/config.toml` -- Cargo toolchain overrides for iOS cross-compilation (CC, AR, linker per target) 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) 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) 307 331 - `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 332 + - `src/lib/components/home/` -- Five home screen components (IdentityListHome, HomeScreen, DIDDocumentScreen, RecoveryInfoScreen, AlertDetailScreen) plus DIDAvatar utility component 333 + - `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 310 335 - `src/routes/+layout.ts` -- `ssr = false; prerender = false` (global SPA config) 311 336 - `svelte.config.js` -- adapter-static with `pages: 'dist'` (SPA mode, matches tauri.conf.json) 312 337 - `vite.config.ts` -- Tauri-compatible Vite server (clearScreen, HMR via TAURI_DEV_HOST, envPrefix)