Move to keyring based session storage to fix macOS auth issues #3

open
opened by dunkirk.sh targeting main

This fixes session persistence issues on macOS where login credentials were not being saved to the keychain. Also standardizes the config directory location across all platforms.

Changes#

Keyring Platform Support

  • Fixed macOS keychain integration by using apple-native feature instead of Linux-only sync-secret-service
  • Added platform-specific keyring features:
    • macOS: apple-native (uses macOS Keychain)
    • Linux: sync-secret-service, vendored (uses GNOME Keyring/KWallet)
    • Windows: windows-native (uses Windows Credential Manager)

Config Path Standardization

  • Moved config directory to ~/.config/tangled on all platforms for consistency
  • Previously used platform-specific paths (e.g., ~/Library/Application Support/tangled on macOS)

Error Messages

  • Improved keychain error messages to be more descriptive
Changed files
+50 -10
crates
tangled-config
+30 -3
Cargo.lock
··· 387 "libc", 388 ] 389 390 [[package]] 391 name = "core-foundation-sys" 392 version = "0.8.7" ··· 1117 source = "registry+https://github.com/rust-lang/crates.io-index" 1118 checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" 1119 dependencies = [ 1120 "dbus-secret-service", 1121 "log", 1122 "openssl", 1123 "zeroize", 1124 ] 1125 ··· 1316 "openssl-probe", 1317 "openssl-sys", 1318 "schannel", 1319 - "security-framework", 1320 "security-framework-sys", 1321 "tempfile", 1322 ] ··· 1831 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1832 dependencies = [ 1833 "bitflags", 1834 - "core-foundation", 1835 "core-foundation-sys", 1836 "libc", 1837 "security-framework-sys", ··· 2055 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2056 dependencies = [ 2057 "bitflags", 2058 - "core-foundation", 2059 "system-configuration-sys", 2060 ] 2061
··· 387 "libc", 388 ] 389 390 + [[package]] 391 + name = "core-foundation" 392 + version = "0.10.1" 393 + source = "registry+https://github.com/rust-lang/crates.io-index" 394 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 395 + dependencies = [ 396 + "core-foundation-sys", 397 + "libc", 398 + ] 399 + 400 [[package]] 401 name = "core-foundation-sys" 402 version = "0.8.7" ··· 1127 source = "registry+https://github.com/rust-lang/crates.io-index" 1128 checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" 1129 dependencies = [ 1130 + "byteorder", 1131 "dbus-secret-service", 1132 "log", 1133 "openssl", 1134 + "security-framework 2.11.1", 1135 + "security-framework 3.5.1", 1136 + "windows-sys 0.60.2", 1137 "zeroize", 1138 ] 1139 ··· 1330 "openssl-probe", 1331 "openssl-sys", 1332 "schannel", 1333 + "security-framework 2.11.1", 1334 "security-framework-sys", 1335 "tempfile", 1336 ] ··· 1845 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1846 dependencies = [ 1847 "bitflags", 1848 + "core-foundation 0.9.4", 1849 + "core-foundation-sys", 1850 + "libc", 1851 + "security-framework-sys", 1852 + ] 1853 + 1854 + [[package]] 1855 + name = "security-framework" 1856 + version = "3.5.1" 1857 + source = "registry+https://github.com/rust-lang/crates.io-index" 1858 + checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" 1859 + dependencies = [ 1860 + "bitflags", 1861 + "core-foundation 0.10.1", 1862 "core-foundation-sys", 1863 "libc", 1864 "security-framework-sys", ··· 2082 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2083 dependencies = [ 2084 "bitflags", 2085 + "core-foundation 0.9.4", 2086 "system-configuration-sys", 2087 ] 2088
+1 -1
Cargo.toml
··· 41 42 # Storage 43 dirs = "5.0" 44 - keyring = { version = "3.6", features = ["sync-secret-service", "vendored"] } 45 46 # Error Handling 47 anyhow = "1.0"
··· 41 42 # Storage 43 dirs = "5.0" 44 + keyring = "3.6" 45 46 # Error Handling 47 anyhow = "1.0"
+9 -1
crates/tangled-config/Cargo.toml
··· 8 [dependencies] 9 anyhow = { workspace = true } 10 dirs = { workspace = true } 11 - keyring = { workspace = true } 12 serde = { workspace = true, features = ["derive"] } 13 serde_json = { workspace = true } 14 toml = { workspace = true } 15 chrono = { workspace = true } 16
··· 8 [dependencies] 9 anyhow = { workspace = true } 10 dirs = { workspace = true } 11 serde = { workspace = true, features = ["derive"] } 12 serde_json = { workspace = true } 13 toml = { workspace = true } 14 chrono = { workspace = true } 15 16 + [target.'cfg(target_os = "macos")'.dependencies] 17 + keyring = { workspace = true, features = ["apple-native"] } 18 + 19 + [target.'cfg(target_os = "linux")'.dependencies] 20 + keyring = { workspace = true, features = ["sync-secret-service", "vendored"] } 21 + 22 + [target.'cfg(target_os = "windows")'.dependencies] 23 + keyring = { workspace = true, features = ["windows-native"] } 24 +
+8 -3
crates/tangled-config/src/config.rs
··· 2 use std::path::{Path, PathBuf}; 3 4 use anyhow::{Context, Result}; 5 - use dirs::config_dir; 6 use serde::{Deserialize, Serialize}; 7 8 #[derive(Debug, Clone, Serialize, Deserialize, Default)] ··· 55 } 56 57 pub fn default_config_path() -> Result<PathBuf> { 58 - let base = config_dir().context("Could not determine platform config directory")?; 59 - Ok(base.join("tangled").join("config.toml")) 60 } 61 62 pub fn load_config(path: Option<&Path>) -> Result<Option<RootConfig>> {
··· 2 use std::path::{Path, PathBuf}; 3 4 use anyhow::{Context, Result}; 5 use serde::{Deserialize, Serialize}; 6 7 #[derive(Debug, Clone, Serialize, Deserialize, Default)] ··· 54 } 55 56 pub fn default_config_path() -> Result<PathBuf> { 57 + // Use ~/.config/tangled on all platforms for consistency 58 + let home = std::env::var("HOME") 59 + .or_else(|_| std::env::var("USERPROFILE")) 60 + .context("Could not determine home directory")?; 61 + Ok(PathBuf::from(home) 62 + .join(".config") 63 + .join("tangled") 64 + .join("config.toml")) 65 } 66 67 pub fn load_config(path: Option<&Path>) -> Result<Option<RootConfig>> {
+2 -2
crates/tangled-config/src/keychain.rs
··· 21 pub fn set_password(&self, secret: &str) -> Result<()> { 22 self.entry()? 23 .set_password(secret) 24 - .map_err(|e| anyhow!("keyring error: {e}")) 25 } 26 27 pub fn get_password(&self) -> Result<String> { 28 self.entry()? 29 .get_password() 30 - .map_err(|e| anyhow!("keyring error: {e}")) 31 } 32 33 pub fn delete_password(&self) -> Result<()> {
··· 21 pub fn set_password(&self, secret: &str) -> Result<()> { 22 self.entry()? 23 .set_password(secret) 24 + .map_err(|e| anyhow!("Failed to save credentials to keychain: {e}")) 25 } 26 27 pub fn get_password(&self) -> Result<String> { 28 self.entry()? 29 .get_password() 30 + .map_err(|e| anyhow!("Failed to load credentials from keychain: {e}")) 31 } 32 33 pub fn delete_password(&self) -> Result<()> {