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): wire deep-link plugin and AppState for OAuth callback (MM-149 phase 2)

Register tauri-plugin-deep-link and tauri-plugin-opener, configure the
custom URL scheme in tauri.conf.json, and wire the on_open_url callback
to a stub handle_deep_link handler. AppState holds pending OAuth flow
and session state for Phase 5 completion.

authored by

Malpercio and committed by
Tangled
8030e1a2 9ab2dfc0

+113
+3
apps/identity-wallet/src-tauri/Cargo.toml
··· 15 15 [dependencies] 16 16 # Tauri-specific — declared locally (not in workspace.dependencies per design plan) 17 17 tauri = { version = "2", features = [] } 18 + tauri-plugin-deep-link = "2" 19 + tauri-plugin-opener = "2" 20 + url = "2" 18 21 # serde/serde_json are in workspace.dependencies (root Cargo.toml lines 30-32) 19 22 serde = { workspace = true } 20 23 serde_json = { workspace = true }
+14
apps/identity-wallet/src-tauri/src/lib.rs
··· 1 1 pub mod device_key; 2 2 pub mod http; 3 3 pub mod keychain; 4 + pub mod oauth; 4 5 5 6 use crypto::{build_did_plc_genesis_op_with_external_signer, CryptoError, DidKeyUri}; 6 7 use serde::{Deserialize, Serialize}; 7 8 use std::sync::LazyLock; 9 + use tauri::Manager; 10 + use tauri_plugin_deep_link::DeepLinkExt; 8 11 9 12 // ── Request / response types ──────────────────────────────────────────────── 10 13 ··· 398 401 #[cfg_attr(mobile, tauri::mobile_entry_point)] 399 402 pub fn run() { 400 403 tauri::Builder::default() 404 + .manage(oauth::AppState::new()) 405 + .plugin(tauri_plugin_deep_link::init()) 406 + .plugin(tauri_plugin_opener::init()) 407 + .setup(|app| { 408 + let app_handle = app.app_handle().clone(); 409 + app.deep_link().on_open_url(move |event| { 410 + let state = app_handle.state::<oauth::AppState>(); 411 + oauth::handle_deep_link(event.urls(), &state); 412 + }); 413 + Ok(()) 414 + }) 401 415 .invoke_handler(tauri::generate_handler![ 402 416 create_account, 403 417 get_or_create_device_key,
+87
apps/identity-wallet/src-tauri/src/oauth.rs
··· 1 + // pattern: Mixed (unavoidable) 2 + // 3 + // Types: AppState, PendingOAuthFlow, OAuthSession, CallbackParams (Functional Core) 4 + // handle_deep_link: Imperative Shell (reads OS callback, routes to pending channel) 5 + 6 + use std::sync::Mutex; 7 + use tracing; 8 + 9 + // ── Shared state ────────────────────────────────────────────────────────────── 10 + 11 + /// App-wide OAuth state registered via `.manage()` in lib.rs. 12 + /// 13 + /// Both fields are Option-wrapped so the state is cleanly empty before any 14 + /// OAuth flow starts and after a flow completes. 15 + pub struct AppState { 16 + /// The pending OAuth flow waiting for the deep-link callback. 17 + /// Set by `start_oauth_flow` before opening Safari; cleared by `handle_deep_link`. 18 + pub pending_auth: Mutex<Option<PendingOAuthFlow>>, 19 + /// The active authenticated session after a successful token exchange. 20 + /// Set by `start_oauth_flow` on success; read by `OAuthClient` for every request. 21 + pub oauth_session: Mutex<Option<OAuthSession>>, 22 + } 23 + 24 + impl AppState { 25 + pub fn new() -> Self { 26 + Self { 27 + pending_auth: Mutex::new(None), 28 + oauth_session: Mutex::new(None), 29 + } 30 + } 31 + } 32 + 33 + // ── Pending flow (stub — filled out in Phase 5) ─────────────────────────────── 34 + 35 + /// State parked inside `AppState.pending_auth` while `start_oauth_flow` waits 36 + /// for the deep-link callback. 37 + /// 38 + /// Phase 5 adds: oneshot::Sender<CallbackParams>, pkce_verifier, csrf_state. 39 + pub struct PendingOAuthFlow { 40 + /// The CSRF state parameter generated at the start of the flow. 41 + /// Used by `handle_deep_link` to validate the callback state. 42 + pub csrf_state: String, 43 + } 44 + 45 + // ── OAuth session (stub — filled out in Phase 5) ────────────────────────────── 46 + 47 + /// Active OAuth session stored after a successful token exchange. 48 + /// 49 + /// Phase 5 adds: access_token, refresh_token, expires_at, dpop_nonce. 50 + pub struct OAuthSession { 51 + pub access_token: String, 52 + pub refresh_token: String, 53 + } 54 + 55 + // ── Callback params ─────────────────────────────────────────────────────────── 56 + 57 + /// Parameters extracted from the OAuth deep-link callback URL. 58 + pub struct CallbackParams { 59 + pub code: String, 60 + pub state: String, 61 + } 62 + 63 + // ── Deep-link handler ───────────────────────────────────────────────────────── 64 + 65 + /// Process URLs received from the deep-link plugin's `on_open_url` event. 66 + /// 67 + /// Filters for the OAuth callback path and logs receipt. Phase 5 completes this 68 + /// by extracting `code`+`state` and sending them on the pending `oneshot` channel. 69 + pub fn handle_deep_link(urls: Vec<url::Url>, app_state: &AppState) { 70 + for url in &urls { 71 + let scheme = url.scheme(); 72 + let path = url.path(); 73 + 74 + if scheme == "dev.malpercio.identitywallet" && path == "/oauth/callback" { 75 + tracing::info!(url = %url, "OAuth deep-link callback received"); 76 + 77 + // Phase 5: extract code+state, validate CSRF, send on oneshot channel. 78 + // For now, just log that the callback arrived. 79 + let _pending = app_state.pending_auth.lock().unwrap(); 80 + tracing::info!("pending_auth slot present: {}", _pending.is_some()); 81 + 82 + return; 83 + } 84 + 85 + tracing::debug!(url = %url, "ignoring non-OAuth deep-link"); 86 + } 87 + }
+9
apps/identity-wallet/src-tauri/tauri.conf.json
··· 21 21 }, 22 22 "bundle": { 23 23 "active": true 24 + }, 25 + "plugins": { 26 + "deep-link": { 27 + "mobile": [ 28 + { 29 + "scheme": ["dev.malpercio.identitywallet"] 30 + } 31 + ] 32 + } 24 33 } 25 34 }