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>
57 files changed,
+1693 insertions,
-122 deletions
| 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 |
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 |
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 |
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 |
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 |
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 |
5403 |
|
"thiserror 1.0.69",
|
| 5402 |
5404 |
|
"tokio",
|
| 5403 |
5405 |
|
"tracing",
|
|
5406 |
+ |
"unicode-normalization",
|
| 5404 |
5407 |
|
"urlencoding",
|
| 5405 |
5408 |
|
"uuid",
|
| 5406 |
5409 |
|
]
|
| 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
|
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 |
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 |
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
|