use keyring::Entry; /// 令牌存取:Windows → Credential Manager,macOS → Keychain pub struct TokenStore { service: String, account: String, } impl TokenStore { pub fn new() -> Self { Self::with_names("xafpet", "jwt") } pub fn with_names(service: &str, account: &str) -> Self { Self { service: service.into(), account: account.into() } } pub fn save(&self, token: &str) -> Result<(), String> { Entry::new(&self.service, &self.account) .map_err(|e| e.to_string())? .set_password(token) .map_err(|e| e.to_string()) } pub fn load(&self) -> Result, String> { let entry = Entry::new(&self.service, &self.account).map_err(|e| e.to_string())?; match entry.get_password() { Ok(p) => Ok(Some(p)), Err(keyring::Error::NoEntry) => Ok(None), Err(e) => Err(e.to_string()), } } pub fn clear(&self) -> Result<(), String> { let entry = Entry::new(&self.service, &self.account).map_err(|e| e.to_string())?; match entry.delete_credential() { Ok(()) => Ok(()), Err(keyring::Error::NoEntry) => Ok(()), Err(e) => Err(e.to_string()), } } } impl Default for TokenStore { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[cfg(not(target_os = "macos"))] #[test] fn roundtrip_save_load_clear() { let store = TokenStore::with_names("xafpet-test", "jwt"); let _ = store.clear(); assert_eq!(store.load().unwrap(), None); store.save("tok-123").unwrap(); assert_eq!(store.load().unwrap(), Some("tok-123".into())); store.save("tok-456").unwrap(); assert_eq!(store.load().unwrap(), Some("tok-456".into())); store.clear().unwrap(); assert_eq!(store.load().unwrap(), None); } }