diff --git a/src/openhuman/channels/providers/telegram/channel_core.rs b/src/openhuman/channels/providers/telegram/channel_core.rs index db6231ae6..df0e2afbb 100644 --- a/src/openhuman/channels/providers/telegram/channel_core.rs +++ b/src/openhuman/channels/providers/telegram/channel_core.rs @@ -15,8 +15,8 @@ impl TelegramChannel { pub fn new(bot_token: String, allowed_users: Vec, mention_only: bool) -> Self { let normalized_allowed = Self::normalize_allowed_users(allowed_users); let pairing = if normalized_allowed.is_empty() { - let guard = PairingGuard::new(true, &[]); - if let Some(code) = guard.pairing_code() { + let (guard, code_opt) = PairingGuard::new(true, &[]); + if let Some(code) = code_opt { println!(" 🔐 Telegram pairing required. One-time bind code: {code}"); println!(" Send `{TELEGRAM_BIND_COMMAND} ` from your Telegram account."); } diff --git a/src/openhuman/channels/providers/telegram/channel_tests.rs b/src/openhuman/channels/providers/telegram/channel_tests.rs index 9066e5b75..eb7b66ce4 100644 --- a/src/openhuman/channels/providers/telegram/channel_tests.rs +++ b/src/openhuman/channels/providers/telegram/channel_tests.rs @@ -215,14 +215,14 @@ fn telegram_user_denied_when_none_of_identities_match() { assert!(!ch.is_any_user_allowed(["unknown", "123456789"])); } -#[test] -fn telegram_pairing_enabled_with_empty_allowlist() { +#[tokio::test] +async fn telegram_pairing_enabled_with_empty_allowlist() { let ch = TelegramChannel::new("t".into(), vec![], false); assert!(ch.pairing_code_active()); } -#[test] -fn telegram_pairing_disabled_with_nonempty_allowlist() { +#[tokio::test] +async fn telegram_pairing_disabled_with_nonempty_allowlist() { let ch = TelegramChannel::new("t".into(), vec!["alice".into()], false); assert!(!ch.pairing_code_active()); } diff --git a/src/openhuman/security/core.rs b/src/openhuman/security/core.rs index c58017fcb..d01afff34 100644 --- a/src/openhuman/security/core.rs +++ b/src/openhuman/security/core.rs @@ -18,7 +18,7 @@ mod tests { let policy = SecurityPolicy::default(); assert_eq!(policy.autonomy, AutonomyLevel::Supervised); - let guard = PairingGuard::new(false, &[]); + let (guard, _) = PairingGuard::new(false, &[]); assert!(!guard.require_pairing()); } diff --git a/src/openhuman/security/pairing.rs b/src/openhuman/security/pairing.rs index 4325aef73..9a4bc35d9 100644 --- a/src/openhuman/security/pairing.rs +++ b/src/openhuman/security/pairing.rs @@ -42,7 +42,7 @@ impl PairingGuard { /// Existing tokens are accepted in both forms: /// - Plaintext (`zc_...`): hashed on load for backward compatibility /// - Already hashed (64-char hex): stored as-is - pub fn new(require_pairing: bool, existing_tokens: &[String]) -> Self { + pub fn new(require_pairing: bool, existing_tokens: &[String]) -> (Self, Option) { let tokens: HashSet = existing_tokens .iter() .map(|t| { @@ -64,12 +64,13 @@ impl PairingGuard { tokens.len(), code.is_some() ); - Self { + let guard = Self { require_pairing, - pairing_code: Arc::new(Mutex::new(code)), + pairing_code: Arc::new(Mutex::new(code.clone())), paired_tokens: Arc::new(Mutex::new(tokens)), failed_attempts: Arc::new(Mutex::new((0, None))), - } + }; + (guard, code) } /// The one-time pairing code (only set when no tokens exist yet). diff --git a/src/openhuman/security/pairing_tests.rs b/src/openhuman/security/pairing_tests.rs index f16a5c454..f8eb4b79f 100644 --- a/src/openhuman/security/pairing_tests.rs +++ b/src/openhuman/security/pairing_tests.rs @@ -5,27 +5,27 @@ use tokio::test; #[test] async fn new_guard_generates_code_when_no_tokens() { - let guard = PairingGuard::new(true, &[]); + let (guard, _) = PairingGuard::new(true, &[]); assert!(guard.pairing_code().is_some()); assert!(!guard.is_paired()); } #[test] async fn new_guard_no_code_when_tokens_exist() { - let guard = PairingGuard::new(true, &["zc_existing".into()]); + let (guard, _) = PairingGuard::new(true, &["zc_existing".into()]); assert!(guard.pairing_code().is_none()); assert!(guard.is_paired()); } #[test] async fn new_guard_no_code_when_pairing_disabled() { - let guard = PairingGuard::new(false, &[]); + let (guard, _) = PairingGuard::new(false, &[]); assert!(guard.pairing_code().is_none()); } #[test] async fn try_pair_correct_code() { - let guard = PairingGuard::new(true, &[]); + let (guard, _) = PairingGuard::new(true, &[]); let code = guard.pairing_code().unwrap().to_string(); let token = guard.try_pair(&code).await.unwrap(); assert!(token.is_some()); @@ -35,7 +35,7 @@ async fn try_pair_correct_code() { #[test] async fn try_pair_wrong_code() { - let guard = PairingGuard::new(true, &[]); + let (guard, _) = PairingGuard::new(true, &[]); let result = guard.try_pair("000000").await.unwrap(); // Might succeed if code happens to be 000000, but extremely unlikely // Just check it returns Ok(None) normally @@ -44,14 +44,14 @@ async fn try_pair_wrong_code() { #[test] async fn try_pair_empty_code() { - let guard = PairingGuard::new(true, &[]); + let (guard, _) = PairingGuard::new(true, &[]); assert!(guard.try_pair("").await.unwrap().is_none()); } #[test] async fn is_authenticated_with_valid_token() { // Pass plaintext token — PairingGuard hashes it on load - let guard = PairingGuard::new(true, &["zc_valid".into()]); + let (guard, _) = PairingGuard::new(true, &["zc_valid".into()]); assert!(guard.is_authenticated("zc_valid")); } @@ -59,26 +59,26 @@ async fn is_authenticated_with_valid_token() { async fn is_authenticated_with_prehashed_token() { // Pass an already-hashed token (64 hex chars) let hashed = hash_token("zc_valid"); - let guard = PairingGuard::new(true, &[hashed]); + let (guard, _) = PairingGuard::new(true, &[hashed]); assert!(guard.is_authenticated("zc_valid")); } #[test] async fn is_authenticated_with_invalid_token() { - let guard = PairingGuard::new(true, &["zc_valid".into()]); + let (guard, _) = PairingGuard::new(true, &["zc_valid".into()]); assert!(!guard.is_authenticated("zc_invalid")); } #[test] async fn is_authenticated_when_pairing_disabled() { - let guard = PairingGuard::new(false, &[]); + let (guard, _) = PairingGuard::new(false, &[]); assert!(guard.is_authenticated("anything")); assert!(guard.is_authenticated("")); } #[test] async fn tokens_returns_hashes() { - let guard = PairingGuard::new(true, &["zc_a".into(), "zc_b".into()]); + let (guard, _) = PairingGuard::new(true, &["zc_a".into(), "zc_b".into()]); let tokens = guard.tokens(); assert_eq!(tokens.len(), 2); // Tokens should be stored as 64-char hex hashes, not plaintext @@ -91,7 +91,7 @@ async fn tokens_returns_hashes() { #[test] async fn pair_then_authenticate() { - let guard = PairingGuard::new(true, &[]); + let (guard, _) = PairingGuard::new(true, &[]); let code = guard.pairing_code().unwrap().to_string(); let token = guard.try_pair(&code).await.unwrap().unwrap(); assert!(guard.is_authenticated(&token)); @@ -203,7 +203,7 @@ async fn generate_token_has_prefix_and_hex_payload() { #[test] async fn brute_force_lockout_after_max_attempts() { - let guard = PairingGuard::new(true, &[]); + let (guard, _) = PairingGuard::new(true, &[]); // Exhaust all attempts with wrong codes for i in 0..MAX_PAIR_ATTEMPTS { let result = guard.try_pair(&format!("wrong_{i}")).await; @@ -225,7 +225,7 @@ async fn brute_force_lockout_after_max_attempts() { #[test] async fn correct_code_resets_failed_attempts() { - let guard = PairingGuard::new(true, &[]); + let (guard, _) = PairingGuard::new(true, &[]); let code = guard.pairing_code().unwrap().to_string(); // Fail a few times for _ in 0..3 { @@ -238,7 +238,7 @@ async fn correct_code_resets_failed_attempts() { #[test] async fn lockout_returns_remaining_seconds() { - let guard = PairingGuard::new(true, &[]); + let (guard, _) = PairingGuard::new(true, &[]); for _ in 0..MAX_PAIR_ATTEMPTS { let _ = guard.try_pair("wrong").await; }