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.

feat(identity-wallet): spawn 15-minute PLC monitoring timer

Implements Task 1 of Phase 2: Monitor Lifecycle. Adds run_monitoring_loop
function to plc_monitor.rs that spawns a background task running the
monitoring loop with a 15-minute interval. The loop:
- Skips the first immediate tick to allow app initialization
- Runs MissedTickBehavior::Delay to prevent burst-firing after iOS suspension
- Checks all managed identities every 15 minutes
- Emits "plc_alert" Tauri events when unauthorized changes are detected

The monitoring loop is spawned in lib.rs setup closure alongside the existing
session restore task.

Verification:
- Rust compilation: cargo check passes without errors
- Existing tests: serialization and monitor creation tests still pass
- Type system: all trait imports (Manager, Emitter) in place

Malpercio 616f3973 1e99565a

+102 -3
+4
apps/identity-wallet/src-tauri/src/lib.rs
··· 791 791 }); 792 792 } 793 793 794 + // Start PLC monitoring timer (15-minute interval) 795 + let monitor_handle = app.handle().clone(); 796 + tauri::async_runtime::spawn(plc_monitor::run_monitoring_loop(monitor_handle)); 797 + 794 798 Ok(()) 795 799 }) 796 800 .invoke_handler(tauri::generate_handler![
+43
apps/identity-wallet/src-tauri/src/plc_monitor.rs
··· 3 3 use crate::pds_client::PdsClient; 4 4 use crypto::{diff_audit_logs, parse_audit_log, verify_plc_operation, AuditEntry, DidKeyUri}; 5 5 use serde::Serialize; 6 + use std::time::Duration; 7 + use tauri::{Emitter, Manager}; 8 + use tokio::time::{interval, MissedTickBehavior}; 6 9 7 10 /// An unauthorized PLC operation detected by the monitor. 8 11 #[derive(Debug, Clone, Serialize)] ··· 189 192 } 190 193 } 191 194 None 195 + } 196 + 197 + const MONITOR_INTERVAL_SECS: u64 = 15 * 60; // 15 minutes 198 + 199 + /// Run a single monitoring cycle. Extracted from the loop for testability. 200 + /// Returns the list of identity statuses with any alerts. 201 + pub async fn run_monitoring_cycle(monitor: &PlcMonitor<'_>) -> Vec<IdentityStatus> { 202 + match monitor.check_all().await { 203 + Ok(statuses) => statuses, 204 + Err(e) => { 205 + tracing::warn!(error = %e, "Monitoring cycle check_all failed"); 206 + vec![] 207 + } 208 + } 209 + } 210 + 211 + /// Run the PLC monitoring loop. Spawned once during app setup. 212 + /// Checks all managed identities every 15 minutes and emits "plc_alert" 213 + /// events to the frontend when unauthorized changes are detected. 214 + pub async fn run_monitoring_loop(app_handle: tauri::AppHandle) { 215 + let mut interval = interval(Duration::from_secs(MONITOR_INTERVAL_SECS)); 216 + // Don't burst-fire missed ticks after iOS suspension 217 + interval.set_missed_tick_behavior(MissedTickBehavior::Delay); 218 + // Skip the first immediate tick — let the app finish initializing 219 + interval.tick().await; 220 + 221 + loop { 222 + interval.tick().await; 223 + 224 + let state = app_handle.state::<crate::oauth::AppState>(); 225 + let monitor = PlcMonitor::new(state.pds_client()); 226 + let statuses = run_monitoring_cycle(&monitor).await; 227 + 228 + let has_alerts = statuses.iter().any(|s| s.alert_count > 0); 229 + if has_alerts { 230 + if let Err(e) = app_handle.emit("plc_alert", &statuses) { 231 + tracing::warn!(error = %e, "Failed to emit plc_alert event"); 232 + } 233 + } 234 + } 192 235 } 193 236 194 237 /// Tauri IPC command: check all managed identities for unauthorized PLC operations.
+38
apps/identity-wallet/src/lib/ipc.ts
··· 486 486 487 487 export const getDeviceKeyId = (did: string): Promise<string> => 488 488 invoke('get_device_key_id', { did }); 489 + 490 + // ── PLC Monitoring ────────────────────────────────────────────────────────── 491 + 492 + /** 493 + * An unauthorized PLC operation detected by the monitor. 494 + * Matches UnauthorizedChange struct in plc_monitor.rs with #[serde(rename_all = "camelCase")]. 495 + */ 496 + export interface UnauthorizedChange { 497 + /** CID of the unauthorized operation. */ 498 + cid: string; 499 + /** ISO 8601 timestamp when plc.directory accepted the operation. */ 500 + createdAt: string; 501 + /** did:key URI of the key that signed this operation, if identified. */ 502 + signingKey: string | null; 503 + /** The raw PLC operation JSON for display in alert detail. */ 504 + operation: unknown; 505 + } 506 + 507 + /** 508 + * Result of checking a single identity's PLC status. 509 + * Matches IdentityStatus struct in plc_monitor.rs with #[serde(rename_all = "camelCase")]. 510 + */ 511 + export interface IdentityStatus { 512 + did: string; 513 + alertCount: number; 514 + unauthorizedChanges: UnauthorizedChange[]; 515 + } 516 + 517 + /** 518 + * Check all managed identities for unauthorized PLC operations. 519 + * Returns a list of IdentityStatus, one per managed DID. 520 + * 521 + * This is the foreground check command — called by the frontend when the app 522 + * becomes visible (visibilitychange event). It supplements the background 523 + * 15-minute polling timer with immediate checks on app foreground. 524 + */ 525 + export const checkIdentityStatus = (): Promise<IdentityStatus[]> => 526 + invoke('check_identity_status');
+17 -3
apps/identity-wallet/src/routes/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { listen } from '@tauri-apps/api/event'; 3 - import { onMount } from 'svelte'; 3 + import { onMount, onDestroy } from 'svelte'; 4 4 import ModeSelectScreen from '$lib/components/onboarding/ModeSelectScreen.svelte'; 5 5 import RelayConfigScreen from '$lib/components/onboarding/RelayConfigScreen.svelte'; 6 6 import WelcomeScreen from '$lib/components/onboarding/WelcomeScreen.svelte'; ··· 21 21 import ClaimSuccessScreen from '$lib/components/onboarding/ClaimSuccessScreen.svelte'; 22 22 import DIDDocumentScreen from '$lib/components/home/DIDDocumentScreen.svelte'; 23 23 import RecoveryInfoScreen from '$lib/components/home/RecoveryInfoScreen.svelte'; 24 - import { createAccount, listIdentities, type CreateAccountError, type OAuthError, type HomeData, type IdentityInfo, type VerifiedClaimOp, type ClaimResult } from '$lib/ipc'; 24 + import { createAccount, listIdentities, checkIdentityStatus, type CreateAccountError, type OAuthError, type HomeData, type IdentityInfo, type VerifiedClaimOp, type ClaimResult } from '$lib/ipc'; 25 25 import { normalizePlcDocToW3c } from '$lib/did-doc-utils'; 26 26 import IdentityListHome from '$lib/components/home/IdentityListHome.svelte'; 27 27 ··· 94 94 95 95 // ── Relay configuration and OAuth event listener ────────────────────── 96 96 97 + function handleVisibilityChange() { 98 + if (document.visibilityState === 'visible' && step === 'home') { 99 + checkIdentityStatus().catch((e) => { 100 + console.warn('PLC status check failed:', e); 101 + }); 102 + } 103 + } 104 + 97 105 onMount(async () => { 98 106 // If the user has claimed identities, skip to home. 99 107 try { 100 108 const identities = await listIdentities(); 101 109 if (identities.length > 0) { 102 110 step = 'home'; 103 - return; 104 111 } 105 112 } catch (e) { 106 113 console.error('listIdentities failed on mount:', e); ··· 120 127 // Promise, not the unlisten function). Since +page.svelte is the root page and never 121 128 // unmounts during the app lifecycle, the listener persists for the app's lifetime, 122 129 // which is the correct behavior. 130 + 131 + // PLC monitoring: check on app foreground 132 + document.addEventListener('visibilitychange', handleVisibilityChange); 133 + }); 134 + 135 + onDestroy(() => { 136 + document.removeEventListener('visibilitychange', handleVisibilityChange); 123 137 }); 124 138 125 139 // ── Account creation ─────────────────────────────────────────────────────