agentmux_srv/muxbus/
mod.rs1pub mod agent_credentials;
5pub mod cloud_subscriber;
6pub mod delivery_status;
7pub mod discovery;
8pub mod wan_lease;
9pub mod pkce;
10pub mod relay;
11pub mod wan_presence;
12pub mod wan_publish;
13pub mod wan_verify;
14
15use crate::backend::storage::muxbus::MuxBusCredentials;
16use crate::broker::CredentialState;
17
18pub const CREDENTIAL_ID: &str = "muxbus:global";
23
24pub(crate) fn stale_sign_in_reason(
37 broker: Option<&CredentialState>,
38 creds: &MuxBusCredentials,
39 discovered: Option<&discovery::CloudSettings>,
40) -> Option<String> {
41 if discovery::client_id_superseded(discovered, &creds.client_id) {
42 return Some("the sign-in belongs to a user pool AgentMux Cloud no longer uses".to_string());
43 }
44 match broker {
45 Some(CredentialState::NeedsReauth { rejected: true, reason, .. })
46 if !(creds.is_valid() && !creds.nearly_expired()) =>
47 {
48 Some(format!("the sign-in was refused: {reason}"))
49 }
50 _ => None,
51 }
52}
53
54pub(crate) async fn stale_sign_in(creds: &MuxBusCredentials, http: &reqwest::Client) -> Option<String> {
57 let broker = crate::broker::get_global().and_then(|s| s.state(CREDENTIAL_ID));
58 let discovered = discovery::cloud_settings(http).await;
59 stale_sign_in_reason(broker.as_ref(), creds, discovered.as_ref())
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 fn creds(client_id: &str, expires_in: i64) -> MuxBusCredentials {
67 MuxBusCredentials {
68 client_id: client_id.to_string(),
69 access_token: "at".to_string(),
70 refresh_token: "rt".to_string(),
71 expires_at: agentmux_common::time::now_secs() + expires_in,
72 ..Default::default()
73 }
74 }
75
76 fn needs_reauth(rejected: bool) -> CredentialState {
77 CredentialState::NeedsReauth { since_unix: 0, reason: "invalid_grant".to_string(), rejected }
78 }
79
80 fn published(client_id: &str) -> discovery::CloudSettings {
81 let doc = format!(
82 r#"{{"version":1,"api":"https://a.test","ws":"wss://b.test","cognito":{{"domain":"https://c.test","clientId":"{client_id}"}}}}"#
83 );
84 discovery::parse(&doc, "https://a.test").unwrap()
85 }
86
87 #[test]
88 fn a_refused_refresh_is_stale_until_a_fresh_token_is_stored() {
89 let expired = creds("client", -10);
90 assert!(stale_sign_in_reason(Some(&needs_reauth(true)), &expired, None).is_some());
91 assert!(stale_sign_in_reason(Some(&needs_reauth(true)), &creds("client", 60), None).is_some());
93 assert!(stale_sign_in_reason(Some(&needs_reauth(true)), &creds("client", 3600), None).is_none());
95 }
96
97 #[test]
98 fn piled_up_transient_failures_and_healthy_states_are_not_stale() {
99 let expired = creds("client", -10);
100 assert!(stale_sign_in_reason(Some(&needs_reauth(false)), &expired, None).is_none());
101 assert!(stale_sign_in_reason(Some(&CredentialState::Fresh), &expired, None).is_none());
102 let failed = CredentialState::Failed { consecutive_failures: 2, last_error: "net".to_string() };
103 assert!(stale_sign_in_reason(Some(&failed), &expired, None).is_none());
104 assert!(stale_sign_in_reason(None, &expired, None).is_none());
105 }
106
107 #[test]
108 fn a_sign_in_from_a_client_the_cloud_no_longer_publishes_is_stale_even_while_its_token_lasts() {
109 let valid = creds("old-client", 3600);
110 let reason = stale_sign_in_reason(Some(&CredentialState::Fresh), &valid, Some(&published("new-client")));
111 assert!(reason.unwrap().contains("user pool"));
112 assert!(stale_sign_in_reason(Some(&CredentialState::Fresh), &valid, Some(&published("old-client"))).is_none());
113 }
114}