Skip to main content

max / goingson

Audit remediation: observability, adversarial fixes, JS tests, doc comments Multi-round audit improvements: - Observability A: 195 #[instrument(skip_all)] annotations across all commands, schedulers, email clients, OAuth, JMAP, state, db_watcher, notifications - Adversarial fixes: Validate trait wired into commands, email HTML sanitized, LLM typed errors, export path traversal guard, IMAP timeout hardening - JS test infrastructure: 48 automated tests covering AppStateManager, utility functions (escapeHtml, escapeAttr, validateEmail, parseEmailAddress, debounce), PaginationManager, and SelectionManager - Code documentation: module-level docs, public function docs, README - Concurrency: graceful shutdown with CancellationToken, explicit HTTP timeouts on all clients (JMAP 30s, OAuth 15s, IMAP 30s)
Co-Authored-By
Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-14 03:35 UTC
Commit: 7efdd78d199229a2c786d17ca4e4b98c0a960d83
Parent: 90f959e
57 files changed, +1693 insertions, -122 deletions
M Cargo.lock +9 -6
@@ -1771,7 +1771,7 @@
1771 1771
1772 1772 [[package]]
1773 1773 name = "goingson-core"
1774 - version = "0.1.0"
1774 + version = "0.2.1"
1775 1775 dependencies = [
1776 1776 "async-trait",
1777 1777 "chrono",
@@ -1786,7 +1786,7 @@
1786 1786
1787 1787 [[package]]
1788 1788 name = "goingson-db-sqlite"
1789 - version = "0.1.0"
1789 + version = "0.2.1"
1790 1790 dependencies = [
1791 1791 "argon2",
1792 1792 "async-trait",
@@ -1801,7 +1801,7 @@
1801 1801
1802 1802 [[package]]
1803 1803 name = "goingson-desktop"
1804 - version = "0.1.0"
1804 + version = "0.2.1"
1805 1805 dependencies = [
1806 1806 "async-imap",
1807 1807 "async-trait",
@@ -1850,7 +1850,7 @@
1850 1850
1851 1851 [[package]]
1852 1852 name = "goingson-mcp"
1853 - version = "0.1.0"
1853 + version = "0.2.1"
1854 1854 dependencies = [
1855 1855 "chrono",
1856 1856 "dirs",
@@ -1869,7 +1869,7 @@
1869 1869
1870 1870 [[package]]
1871 1871 name = "goingson-plugin-runtime"
1872 - version = "0.1.0"
1872 + version = "0.2.1"
1873 1873 dependencies = [
1874 1874 "async-trait",
1875 1875 "chrono",
@@ -5386,13 +5386,15 @@
5386 5386
5387 5387 [[package]]
5388 5388 name = "synckit-client"
5389 - version = "0.2.0"
5389 + version = "0.2.1"
5390 5390 dependencies = [
5391 5391 "argon2",
5392 5392 "base64 0.22.1",
5393 + "bytes",
5393 5394 "chacha20poly1305",
5394 5395 "chrono",
5395 5396 "keyring",
5397 + "parking_lot",
5396 5398 "rand 0.8.5",
5397 5399 "reqwest 0.12.28",
5398 5400 "serde",
@@ -5401,6 +5403,7 @@
5401 5403 "thiserror 1.0.69",
5402 5404 "tokio",
5403 5405 "tracing",
5406 + "unicode-normalization",
5404 5407 "urlencoding",
5405 5408 "uuid",
5406 5409 ]
@@ -8,6 +8,7 @@
8 8 use chrono::Utc;
9 9 use std::sync::Arc;
10 10 use tauri::Manager;
11 + use tokio_util::sync::CancellationToken;
11 12 use tracing::{debug, error, info, warn};
12 13
13 14 /// Check interval for automated backups (1 minute)
@@ -15,7 +16,7 @@
15 16
16 17 /// Starts the background backup scheduler that creates automatic backups
17 18 /// based on user settings and prunes old backups.
18 - pub async fn start_backup_scheduler(app: tauri::AppHandle) {
19 + pub async fn start_backup_scheduler(app: tauri::AppHandle, cancel: CancellationToken) {
19 20 info!(
20 21 "Starting backup scheduler (check interval: {}s)",
21 22 CHECK_INTERVAL_SECS
@@ -26,7 +27,13 @@
26 27 interval.tick().await;
27 28
28 29 loop {
29 - interval.tick().await;
30 + tokio::select! {
31 + _ = cancel.cancelled() => {
32 + info!("Backup scheduler shutting down");
33 + break;
34 + }
35 + _ = interval.tick() => {}
36 + }
30 37
31 38 // Get app state
32 39 let state = match app.try_state::<Arc<AppState>>() {
@@ -24,7 +24,7 @@
24 24 ///
25 25 /// Watches the SQLite database file and its WAL/SHM files for changes.
26 26 /// When changes are detected, emits a `db:external-change` event to the frontend.
27 - pub fn start_db_watcher(app: tauri::AppHandle) {
27 + pub fn start_db_watcher(app: tauri::AppHandle, shutdown: Arc<AtomicBool>) {
28 28 // Get the database path
29 29 let app_data_dir = match app.path().app_data_dir() {
30 30 Ok(dir) => dir,
@@ -58,10 +58,16 @@
58 58
59 59 // Trailing edge timer: emits a final event after MIN_EVENT_INTERVAL_MS
60 60 // if any changes were dropped during rate-limiting
61 + let trailing_shutdown = shutdown.clone();
61 62 std::thread::spawn(move || {
62 63 loop {
63 64 std::thread::sleep(Duration::from_millis(MIN_EVENT_INTERVAL_MS));
64 65
66 + if trailing_shutdown.load(Ordering::Relaxed) {
67 + info!("Trailing timer shutting down");
68 + break;
69 + }
70 +
65 71 if trailing_pending.swap(false, Ordering::Relaxed) {
66 72 let now = std::time::SystemTime::now()
67 73 .duration_since(std::time::UNIX_EPOCH)
@@ -78,6 +84,7 @@
78 84 }
79 85 });
80 86
87 + let watcher_shutdown = shutdown;
81 88 std::thread::spawn(move || {
82 89 let (tx, rx) = std::sync::mpsc::channel();
83 90
@@ -97,10 +104,15 @@
97 104
98 105 info!(?app_data_dir, "Database watcher started");
99 106
100 - // Process file change events
101 - for result in rx {
102 - match result {
103 - Ok(events) => {
107 + // Process file change events with shutdown checks
108 + loop {
109 + if watcher_shutdown.load(Ordering::Relaxed) {
110 + info!("Database watcher shutting down");
111 + break;
112 + }
113 +
114 + match rx.recv_timeout(Duration::from_secs(1)) {
115 + Ok(Ok(events)) => {
104 116 // Check if any event is for our database files
105 117 let db_changed = events.iter().any(|event| {
106 118 is_db_file(&event.path, &db_path)
@@ -131,9 +143,17 @@
131 143 }
132 144 }
133 145 }
134 - Err(e) => {
146 + Ok(Err(e)) => {
135 147 warn!("File watcher error: {:?}", e);
136 148 }
149 + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
150 + // No events, loop back to check shutdown flag
151 + continue;
152 + }
153 + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
154 + warn!("File watcher channel disconnected");
155 + break;
156 + }
137 157 }
138 158 }
139 159 });
@@ -6,6 +6,7 @@
6 6 use std::sync::Arc;
7 7 use tauri::Manager;
8 8 use tokio::time::{interval, Duration};
9 + use tokio_util::sync::CancellationToken;
9 10 use tracing::{debug, error, info, warn};
10 11
11 12 use crate::commands::sync_email_account_inner;
@@ -27,7 +28,7 @@
27 28 /// - Syncs each account using the existing sync logic
28 29 /// - Logs success/failure for monitoring
29 30 /// - Continues running even if individual syncs fail
30 - pub async fn start_email_sync_scheduler(app: tauri::AppHandle) {
31 + pub async fn start_email_sync_scheduler(app: tauri::AppHandle, cancel: CancellationToken) {
31 32 let mut check_interval = interval(Duration::from_secs(CHECK_INTERVAL_SECS));
32 33
33 34 info!("Email sync scheduler started (checking every {} seconds)", CHECK_INTERVAL_SECS);
@@ -35,7 +36,13 @@
35 36 // Infinite tick loop: sleep for CHECK_INTERVAL_SECS, then check all accounts.
36 37 // The first tick fires immediately (tokio::time::interval behavior).
37 38 loop {
38 - check_interval.tick().await;
39 + tokio::select! {
40 + _ = cancel.cancelled() => {
41 + info!("Email sync scheduler shutting down");
42 + break;
43 + }
44 + _ = check_interval.tick() => {}
45 + }
39 46
40 47 // try_state returns None during startup before AppState is managed.
41 48 // Continuing is safe — we'll pick it up on the next tick once the
A README.md +65