Skip to main content

max / goingson

Drop parking_lot, dirs, dotenvy, futures; upgrade thiserror to 2 Dependency-pruning pass driven by Run 24 dep audit. - parking_lot::{Mutex, RwLock} → std::sync::{Mutex, RwLock}. All acquisition sites use .expect("poisoned") since none of these locks is held across .await or across a panic-risky region. The async-block case in email_sync uses unwrap_or_else(into_inner) defensively. - futures → futures-util (only StreamExt was used; futures-util pulls in just what's needed without the executor and util crates). - dirs and dotenvy were unused; removed. - thiserror 1 → 2. - keyring features pinned to apple-native, linux-native, windows-native so the native backends are explicit. Also: better keychain-restore logging in load_sync_client (was silent when no session existed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-14 19:26 UTC
Commit: 3bb45b73b4bc853e40152021101d1a8b86e7d155
Parent: 94b4d67
10 files changed, +96 insertions, -62 deletions
M Cargo.lock +37 -11
@@ -1887,7 +1887,7 @@ dependencies = [
1887 1887 "strum",
1888 1888 "strum_macros",
1889 1889 "tagtree",
1890 - "thiserror 1.0.69",
1890 + "thiserror 2.0.18",
1891 1891 "uuid",
1892 1892 ]
1893 1893
@@ -1916,10 +1916,9 @@ dependencies = [
1916 1916 "chrono",
1917 1917 "chrono-tz",
1918 1918 "csv",
1919 - "dirs",
1920 1919 "docengine",
1921 1920 "flate2",
1922 - "futures",
1921 + "futures-util",
1923 1922 "goingson-core",
1924 1923 "goingson-db-sqlite",
1925 1924 "goingson-plugin-runtime",
@@ -1933,7 +1932,6 @@ dependencies = [
1933 1932 "notify-debouncer-mini",
1934 1933 "open",
1935 1934 "openssl",
1936 - "parking_lot",
1937 1935 "pter",
1938 1936 "rand 0.9.2",
1939 1937 "reqwest 0.12.28",
@@ -1951,7 +1949,7 @@ dependencies = [
1951 1949 "tauri-plugin-window-state",
1952 1950 "tempfile",
1953 1951 "theme-common",
1954 - "thiserror 1.0.69",
1952 + "thiserror 2.0.18",
1955 1953 "tokio",
1956 1954 "tokio-native-tls",
1957 1955 "tokio-util",
@@ -1974,7 +1972,7 @@ dependencies = [
1974 1972 "serde",
1975 1973 "serde_json",
1976 1974 "tempfile",
1977 - "thiserror 1.0.69",
1975 + "thiserror 2.0.18",
1978 1976 "tokio",
1979 1977 "toml 0.8.2",
1980 1978 "tracing",
@@ -2723,7 +2721,12 @@ version = "3.6.3"
2723 2721 source = "registry+https://github.com/rust-lang/crates.io-index"
2724 2722 checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
2725 2723 dependencies = [
2724 + "byteorder",
2725 + "linux-keyutils",
2726 2726 "log",
2727 + "security-framework 2.11.1",
2728 + "security-framework 3.7.0",
2729 + "windows-sys 0.60.2",
2727 2730 "zeroize",
2728 2731 ]
2729 2732
@@ -2870,6 +2873,16 @@ dependencies = [
2870 2873 ]
2871 2874
2872 2875 [[package]]
2876 + name = "linux-keyutils"
2877 + version = "0.2.5"
2878 + source = "registry+https://github.com/rust-lang/crates.io-index"
2879 + checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590"
2880 + dependencies = [
2881 + "bitflags 2.11.0",
2882 + "libc",
2883 + ]
2884 +
2885 + [[package]]
2873 2886 name = "linux-raw-sys"
2874 2887 version = "0.12.1"
2875 2888 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3134,7 +3147,7 @@ dependencies = [
3134 3147 "openssl-probe",
3135 3148 "openssl-sys",
3136 3149 "schannel",
3137 - "security-framework",
3150 + "security-framework 3.7.0",
3138 3151 "security-framework-sys",
3139 3152 "tempfile",
3140 3153 ]
@@ -4636,7 +4649,7 @@ dependencies = [
4636 4649 "openssl-probe",
4637 4650 "rustls-pki-types",
4638 4651 "schannel",
4639 - "security-framework",
4652 + "security-framework 3.7.0",
4640 4653 ]
4641 4654
4642 4655 [[package]]
@@ -4663,7 +4676,7 @@ dependencies = [
4663 4676 "rustls-native-certs",
4664 4677 "rustls-platform-verifier-android",
4665 4678 "rustls-webpki",
4666 - "security-framework",
4679 + "security-framework 3.7.0",
4667 4680 "security-framework-sys",
4668 4681 "webpki-root-certs",
4669 4682 "windows-sys 0.61.2",
@@ -4677,9 +4690,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
4677 4690
4678 4691 [[package]]
4679 4692 name = "rustls-webpki"
4680 - version = "0.103.10"
4693 + version = "0.103.13"
4681 4694 source = "registry+https://github.com/rust-lang/crates.io-index"
4682 - checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
4695 + checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
4683 4696 dependencies = [
4684 4697 "ring",
4685 4698 "rustls-pki-types",
@@ -4790,6 +4803,19 @@ dependencies = [
4790 4803
4791 4804 [[package]]
4792 4805 name = "security-framework"
4806 + version = "2.11.1"
4807 + source = "registry+https://github.com/rust-lang/crates.io-index"
4808 + checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
4809 + dependencies = [
4810 + "bitflags 2.11.0",
4811 + "core-foundation 0.9.4",
4812 + "core-foundation-sys",
4813 + "libc",
4814 + "security-framework-sys",
4815 + ]
4816 +
4817 + [[package]]
4818 + name = "security-framework"
4793 4819 version = "3.7.0"
4794 4820 source = "registry+https://github.com/rust-lang/crates.io-index"
4795 4821 checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
M Cargo.toml +3 -9
@@ -21,7 +21,7 @@ uuid = { version = "1.16", features = ["v4", "v5", "serde"] }
21 21 serde = { version = "1.0.228", features = ["derive"] }
22 22 serde_json = "1.0.149"
23 23 async-trait = "0.1"
24 - thiserror = "1"
24 + thiserror = "2"
25 25
26 26 # Async runtime
27 27 tokio = { version = "1.49", features = ["rt-multi-thread", "macros"] }
@@ -36,7 +36,7 @@ argon2 = "0.5"
36 36 async-imap = { version = "0.11", default-features = false, features = ["runtime-tokio"] }
37 37 tokio-native-tls = "0.3"
38 38 tokio-util = "0.7"
39 - futures = "0.3"
39 + futures-util = { version = "0.3", default-features = false, features = ["std", "async-await"] }
40 40 mailparse = "0.16"
41 41 lettre = { version = "0.11", default-features = false, features = ["tokio1", "tokio1-native-tls", "smtp-transport", "builder"] }
42 42
@@ -49,10 +49,7 @@ base64 = "0.22"
49 49 sha2 = "0.10"
50 50
51 51 # Secure credential storage
52 - keyring = "3"
53 -
54 - # Config
55 - dotenvy = "0.15"
52 + keyring = { version = "3", features = ["apple-native", "linux-native", "windows-native"] }
56 53
57 54 # Tauri
58 55 tauri = "2.10.2"
@@ -90,9 +87,6 @@ open = "5"
90 87 # Markdown rendering + HTML sanitization
91 88 docengine = { path = "../../MNW/shared/docengine" }
92 89
93 - # Filesystem
94 - dirs = "6"
95 -
96 90 # Logging
97 91 tracing = "0.1"
98 92 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
@@ -44,7 +44,7 @@ uuid = { workspace = true }
44 44 async-imap = { workspace = true }
45 45 tokio-native-tls = { workspace = true }
46 46 tokio-util = { workspace = true }
47 - futures = { workspace = true }
47 + futures-util = { workspace = true }
48 48 mailparse = { workspace = true }
49 49 lettre = { workspace = true }
50 50
@@ -72,8 +72,6 @@ flate2 = { workspace = true }
72 72 # Theme loading
73 73 theme-common = { workspace = true }
74 74 toml = { workspace = true }
75 - dirs = { workspace = true }
76 -
77 75 # Browser opening
78 76 open = { workspace = true }
79 77
@@ -85,7 +83,6 @@ keyring = { workspace = true }
85 83
86 84 # HTML email to readable markdown
87 85 pter = { path = "../../../pter" }
88 - parking_lot = "0.12.5"
89 86
90 87 # === Desktop-only dependencies (not available on iOS/Android) ===
91 88
@@ -46,7 +46,7 @@ pub async fn sync_email_account_inner(
46 46 full_sync: Option<bool>,
47 47 ) -> Result<SyncResponse, ApiError> {
48 48 // Acquire per-account lock to prevent concurrent syncs on the same account
49 - if !state.email_sync_locks.lock().insert(id) {
49 + if !state.email_sync_locks.lock().expect("email_sync_locks poisoned").insert(id) {
50 50 return Err(ApiError::bad_request("Sync already in progress for this account"));
51 51 }
52 52
@@ -67,8 +67,12 @@ pub async fn sync_email_account_inner(
67 67 Ok(result)
68 68 }.await;
69 69
70 - // Always release the lock (parking_lot::Mutex — safe even if async block panicked)
71 - state.email_sync_locks.lock().remove(&id);
70 + // Always release the lock. The lock is not held across the await, so it cannot be poisoned
71 + // by an async-block panic; unwrap_or_else handles the unreachable poison case defensively.
72 + state.email_sync_locks
73 + .lock()
74 + .unwrap_or_else(|p| p.into_inner())
75 + .remove(&id);
72 76
73 77 result
74 78 }
@@ -128,7 +128,7 @@ pub async fn start_oauth(
128 128
129 129 // Store PKCE verifier and flow details server-side (never sent to frontend)
130 130 {
131 - let mut flows = state.pending_oauth_flows.lock();
131 + let mut flows = state.pending_oauth_flows.lock().expect("pending_oauth_flows poisoned");
132 132 flows.insert(start_result.state.clone(), crate::state::PendingOAuthFlow {
133 133 code_verifier: start_result.code_verifier,
134 134 provider_id: provider_id.clone(),
@@ -163,7 +163,7 @@ pub async fn complete_oauth(
163 163 ) -> Result<OAuthCompleteResponse, ApiError> {
164 164 // Look up and consume the pending flow by state token (CSRF validation)
165 165 let flow = {
166 - let mut flows = state.pending_oauth_flows.lock();
166 + let mut flows = state.pending_oauth_flows.lock().expect("pending_oauth_flows poisoned");
167 167 flows.remove(&input.state)
168 168 }.ok_or_else(|| ApiError::bad_request("Invalid or expired OAuth state token"))?;
169 169
@@ -72,7 +72,7 @@ pub struct SyncSettingsInput {
72 72
73 73 /// Extract the sync client from state. Clones the Arc for use across await points.
74 74 fn get_sync_client(state: &AppState) -> Option<std::sync::Arc<synckit_client::SyncKitClient>> {
75 - state.sync_client.read().clone()
75 + state.sync_client.read().expect("sync_client poisoned").clone()
76 76 }
77 77
78 78 fn require_sync_client(state: &AppState) -> Result<std::sync::Arc<synckit_client::SyncKitClient>, ApiError> {
@@ -171,7 +171,7 @@ pub async fn sync_start_auth(
171 171
172 172 // Store PKCE verifier server-side (never sent to frontend)
173 173 {
174 - let mut flows = state.pending_oauth_flows.lock();
174 + let mut flows = state.pending_oauth_flows.lock().expect("pending_oauth_flows poisoned");
175 175 flows.insert(csrf_state.clone(), crate::state::PendingOAuthFlow {
176 176 code_verifier,
177 177 provider_id: "synckit".to_string(),
@@ -197,7 +197,7 @@ pub async fn sync_complete_auth(
197 197
198 198 // Look up and consume the pending flow by state token (CSRF + PKCE validation)
199 199 let flow = {
200 - let mut flows = state.pending_oauth_flows.lock();
200 + let mut flows = state.pending_oauth_flows.lock().expect("pending_oauth_flows poisoned");
201 201 flows.remove(&input.state)
202 202 }.ok_or_else(|| ApiError::bad_request("Invalid or expired OAuth state token"))?;
203 203
@@ -381,10 +381,16 @@ pub async fn sync_subscribe(
381 381 return Err(ApiError::bad_request("Not authenticated"));
382 382 }
383 383
384 - let response = client
384 + let response = match client
385 385 .create_subscription_checkout("standard", &interval)
386 386 .await
387 - .map_api_err("Failed to create checkout", ApiError::external_service)?;
387 + {
388 + Ok(r) => r,
389 + Err(e) => {
390 + tracing::error!(error = %e, debug = ?e, "Subscription checkout failed");
391 + return Err(ApiError::external_service(format!("Failed to create checkout: {e}")));
392 + }
393 + };
388 394
389 395 // Open in default browser
390 396 if let Err(e) = open::that(&response.checkout_url) {
@@ -3,7 +3,7 @@
3 3 use async_imap::Client;
4 4 use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
5 5 use chrono::{DateTime, TimeZone, Utc};
6 - use futures::StreamExt;
6 + use futures_util::StreamExt;
7 7 use goingson_core::{EmailAccount, FolderSyncState};
8 8 use tokio::net::TcpStream;
9 9 use tokio_native_tls::native_tls::TlsConnector as NativeTlsConnector;
@@ -651,7 +651,7 @@ impl ImapClient {
651 651 .await
652 652 .map_err(|e| format!("List folders error: {}", e))?;
653 653
654 - use futures::StreamExt;
654 + use futures_util::StreamExt;
655 655 let folders: Vec<String> = folders_stream
656 656 .filter_map(|result| async {
657 657 result.ok().map(|name| name.name().to_string())
@@ -17,8 +17,7 @@ use goingson_db_sqlite::{
17 17 };
18 18 use sqlx::SqlitePool;
19 19 use std::path::PathBuf;
20 - use std::sync::Arc;
21 - use parking_lot::RwLock;
20 + use std::sync::{Arc, Mutex, RwLock};
22 21 use tokio::sync::Mutex as TokioMutex;
23 22 use synckit_client::{SyncKitClient, SyncKitConfig};
24 23 use tauri::{AppHandle, Manager};
@@ -53,11 +52,11 @@ pub struct AppState {
53 52 pub sync_client: RwLock<Option<Arc<SyncKitClient>>>,
54 53 pub sync_lock: Arc<TokioMutex<()>>,
55 54 /// Per-account email sync locks to prevent concurrent syncs on the same account.
56 - pub email_sync_locks: Arc<parking_lot::Mutex<std::collections::HashSet<goingson_core::EmailAccountId>>>,
55 + pub email_sync_locks: Arc<Mutex<std::collections::HashSet<goingson_core::EmailAccountId>>>,
57 56 /// Per-account token refresh locks to prevent concurrent refreshes.
58 - pub token_refresh_locks: Arc<parking_lot::Mutex<std::collections::HashMap<uuid::Uuid, Arc<TokioMutex<()>>>>>,
57 + pub token_refresh_locks: Arc<Mutex<std::collections::HashMap<uuid::Uuid, Arc<TokioMutex<()>>>>>,
59 58 /// Pending OAuth flows keyed by state token (CSRF + PKCE verifier stored server-side).
60 - pub pending_oauth_flows: Arc<parking_lot::Mutex<std::collections::HashMap<String, PendingOAuthFlow>>>,
59 + pub pending_oauth_flows: Arc<Mutex<std::collections::HashMap<String, PendingOAuthFlow>>>,
61 60 pub data_dir: PathBuf,
62 61 }
63 62
@@ -162,16 +161,16 @@ impl AppState {
162 161 sync_accounts,
163 162 sync_client: RwLock::new(sync_client.map(Arc::new)),
164 163 sync_lock: Arc::new(TokioMutex::new(())),
165 - email_sync_locks: Arc::new(parking_lot::Mutex::new(std::collections::HashSet::new())),
166 - token_refresh_locks: Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())),
167 - pending_oauth_flows: Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())),
164 + email_sync_locks: Arc::new(Mutex::new(std::collections::HashSet::new())),
165 + token_refresh_locks: Arc::new(Mutex::new(std::collections::HashMap::new())),
166 + pending_oauth_flows: Arc::new(Mutex::new(std::collections::HashMap::new())),
168 167 data_dir: app_data_dir,
169 168 })
170 169 }
171 170
172 171 /// Gets or creates a per-account token refresh lock.
173 172 pub fn token_refresh_lock(&self, account_id: uuid::Uuid) -> Arc<TokioMutex<()>> {
174 - let mut locks = self.token_refresh_locks.lock();
173 + let mut locks = self.token_refresh_locks.lock().expect("token_refresh_locks poisoned");
175 174 locks.entry(account_id)
176 175 .or_insert_with(|| Arc::new(TokioMutex::new(())))
177 176 .clone()
@@ -240,18 +239,25 @@ fn load_sync_client(data_dir: &std::path::Path) -> Option<SyncKitClient> {
240 239 let client = SyncKitClient::new(SyncKitConfig { server_url, api_key });
241 240
242 241 // Try to restore session from keychain
243 - if let Some(creds) = crate::oauth::CredentialStore::get_sync_token() {
244 - client.restore_session(&creds.token, creds.user_id, creds.app_id);
245 - if client.is_token_expired() {
246 - warn!("Stored sync token is expired, clearing session");
247 - client.clear_session();
248 - } else {
249 - match client.try_load_key_from_keychain() {
250 - Ok(true) => info!("Sync encryption key loaded from keychain"),
251 - Ok(false) => debug!("No sync encryption key in keychain"),
252 - Err(e) => warn!("Failed to load sync encryption key: {}", e),
242 + match crate::oauth::CredentialStore::get_sync_token() {
243 + Some(creds) => {
244 + info!("Restoring sync session from keychain (user={})", creds.user_id);
245 + client.restore_session(&creds.token, creds.user_id, creds.app_id);
246 + if client.is_token_expired() {
247 + warn!("Stored sync token is expired, clearing session");
248 + client.clear_session();
249 + } else {
250 + info!("Sync session restored successfully");
251 + match client.try_load_key_from_keychain() {
252 + Ok(true) => info!("Sync encryption key loaded from keychain"),
253 + Ok(false) => debug!("No sync encryption key in keychain"),
254 + Err(e) => warn!("Failed to load sync encryption key: {}", e),
255 + }
253 256 }
254 257 }
258 + None => {
259 + info!("No sync token found in keychain — user will need to authenticate");
260 + }
255 261 }
256 262
257 263 Some(client)
@@ -72,7 +72,7 @@ pub async fn start_sync_scheduler(app: tauri::AppHandle, cancel: CancellationTok
72 72 }
73 73 };
74 74
75 - let client: Arc<synckit_client::SyncKitClient> = match state.sync_client.read().clone() {
75 + let client: Arc<synckit_client::SyncKitClient> = match state.sync_client.read().expect("sync_client poisoned").clone() {
76 76 Some(c) => c,
77 77 None => continue,
78 78 };
@@ -8,8 +8,8 @@ use goingson_core::{ProjectId, UserId};
8 8 use goingson_db_sqlite::{
9 9 init_pool, run_migrations,
10 10 SqliteAttachmentRepository, SqliteBackupSettingsRepository, SqliteContactRepository,
11 - SqliteEmailAccountRepository, SqliteEmailRepository, SqliteEventRepository,
12 - SqliteMilestoneRepository,
11 + SqliteDailyNoteRepository, SqliteEmailAccountRepository, SqliteEmailRepository,
12 + SqliteEventRepository, SqliteMilestoneRepository,
13 13 SqliteProjectRepository, SqliteSavedViewRepository, SqliteMonthlyReviewRepository,
14 14 SqliteSearchRepository, SqliteStatsRepository, SqliteSyncAccountRepository,
15 15 SqliteTaskRepository, SqliteWeeklyReviewRepository,
@@ -62,6 +62,7 @@ pub async fn setup_test_state() -> (Arc<AppState>, UserId) {
62 62 emails: Arc::new(SqliteEmailRepository::new(pool.clone())),
63 63 email_accounts: Arc::new(SqliteEmailAccountRepository::new(pool.clone())),
64 64 contacts: Arc::new(SqliteContactRepository::new(pool.clone())),
65 + daily_notes: Arc::new(SqliteDailyNoteRepository::new(pool.clone())),
65 66 attachments: Arc::new(SqliteAttachmentRepository::new(pool.clone())),
66 67 stats: Arc::new(SqliteStatsRepository::new(pool.clone())),
67 68 search: Arc::new(SqliteSearchRepository::new(pool.clone())),
@@ -71,11 +72,11 @@ pub async fn setup_test_state() -> (Arc<AppState>, UserId) {
71 72 monthly_reviews: Arc::new(SqliteMonthlyReviewRepository::new(pool.clone())),
72 73 backup_settings: Arc::new(SqliteBackupSettingsRepository::new(pool.clone())),
73 74 sync_accounts: Arc::new(SqliteSyncAccountRepository::new(pool.clone())),
74 - sync_client: parking_lot::RwLock::new(None),
75 + sync_client: std::sync::RwLock::new(None),
75 76 sync_lock: Arc::new(tokio::sync::Mutex::new(())),
76 - email_sync_locks: Arc::new(parking_lot::Mutex::new(std::collections::HashSet::new())),
77 - token_refresh_locks: Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())),
78 - pending_oauth_flows: Arc::new(parking_lot::Mutex::new(std::collections::HashMap::new())),
77 + email_sync_locks: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())),
78 + token_refresh_locks: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
79 + pending_oauth_flows: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
79 80 data_dir: std::path::PathBuf::from("/tmp/goingson-test"),
80 81 };
81 82