im-core 0.1.0

Rust IM SDK for Awiki clients built on Agent Network Protocol (ANP)
Documentation
use super::state::AuthStateSnapshot;

pub(crate) trait SessionProvider {
    fn ensure_session(
        &self,
        scope: crate::auth::AuthScope,
    ) -> crate::ImResult<crate::auth::SessionBundle>;

    fn refresh_session(&self) -> crate::ImResult<crate::auth::SessionUpdate>;

    fn status(&self) -> crate::ImResult<crate::auth::AuthStatus>;
}

pub(crate) trait AsyncSessionProvider {
    async fn ensure_session(
        &self,
        scope: crate::auth::AuthScope,
    ) -> crate::ImResult<crate::auth::SessionBundle>;

    async fn refresh_session(&self) -> crate::ImResult<crate::auth::SessionUpdate>;

    async fn status(&self) -> crate::ImResult<crate::auth::AuthStatus>;
}

pub(crate) struct FileSessionProvider<'a> {
    client: &'a crate::core::ImClient,
}

impl<'a> FileSessionProvider<'a> {
    pub(crate) fn new(client: &'a crate::core::ImClient) -> Self {
        Self { client }
    }

    fn snapshot(&self) -> crate::ImResult<SessionSnapshot> {
        let runtime = self.client.runtime();
        let did_document_available = runtime.key_provider.optional_did_document()?.is_some();
        let private_key_available = runtime.key_provider.default_signing_private_pem().is_ok();
        let auth_state = runtime.key_provider.auth_state()?;
        Ok(SessionSnapshot {
            subject: self.client.did().clone(),
            ready_for_auth: self.client.current_identity().readiness.ready_for_auth,
            ready_for_messaging: self.client.current_identity().readiness.ready_for_messaging,
            did_document_available,
            private_key_available,
            auth_state,
        })
    }

    async fn snapshot_async(&self) -> crate::ImResult<SessionSnapshot> {
        self.snapshot()
    }
}

impl SessionProvider for FileSessionProvider<'_> {
    fn ensure_session(
        &self,
        scope: crate::auth::AuthScope,
    ) -> crate::ImResult<crate::auth::SessionBundle> {
        let snapshot = self.snapshot()?;
        snapshot.ensure_ready(scope)?;
        Ok(crate::auth::SessionBundle {
            subject: snapshot.subject,
            scope,
            expires_at: snapshot.auth_state.expires_at.clone(),
            refreshed: false,
            bearer_token: snapshot.auth_state.bearer_token,
        })
    }

    fn refresh_session(&self) -> crate::ImResult<crate::auth::SessionUpdate> {
        let snapshot = self.snapshot()?;
        snapshot.ensure_refresh_ready()?;
        let mut transport = crate::internal::transport::CoreHttpTransport::new(self.client);
        transport.refresh_jwt()?;
        let refreshed = self.snapshot()?;
        Ok(crate::auth::SessionUpdate {
            subject: snapshot.subject,
            previous_expires_at: snapshot.auth_state.expires_at.clone(),
            new_expires_at: refreshed.auth_state.expires_at,
            refreshed: true,
            bearer_token: refreshed.auth_state.bearer_token,
        })
    }

    fn status(&self) -> crate::ImResult<crate::auth::AuthStatus> {
        let snapshot = self.snapshot()?;
        Ok(crate::auth::AuthStatus {
            subject: snapshot.subject.clone(),
            has_session: snapshot.ready_for_auth
                && snapshot.did_document_available
                && snapshot.private_key_available
                && snapshot.auth_state.has_valid_token,
            expires_at: snapshot.auth_state.expires_at.clone(),
            needs_refresh: snapshot.ready_for_auth
                && snapshot.did_document_available
                && snapshot.private_key_available
                && (!snapshot.auth_state.has_token || snapshot.auth_state.needs_refresh),
            warnings: snapshot.warnings(),
        })
    }
}

impl<T> SessionProvider for &T
where
    T: SessionProvider + ?Sized,
{
    fn ensure_session(
        &self,
        scope: crate::auth::AuthScope,
    ) -> crate::ImResult<crate::auth::SessionBundle> {
        (**self).ensure_session(scope)
    }

    fn refresh_session(&self) -> crate::ImResult<crate::auth::SessionUpdate> {
        (**self).refresh_session()
    }

    fn status(&self) -> crate::ImResult<crate::auth::AuthStatus> {
        (**self).status()
    }
}

impl AsyncSessionProvider for FileSessionProvider<'_> {
    async fn ensure_session(
        &self,
        scope: crate::auth::AuthScope,
    ) -> crate::ImResult<crate::auth::SessionBundle> {
        let snapshot = self.snapshot_async().await?;
        snapshot.ensure_identity_ready(scope)?;
        if snapshot.auth_state.has_valid_token && !snapshot.auth_state.needs_refresh {
            return Ok(crate::auth::SessionBundle {
                subject: snapshot.subject,
                scope,
                expires_at: snapshot.auth_state.expires_at.clone(),
                refreshed: false,
                bearer_token: snapshot.auth_state.bearer_token,
            });
        }

        let mut transport = crate::internal::transport::CoreHttpTransport::new(self.client);
        transport.refresh_jwt_async().await?;
        let refreshed = self.snapshot_async().await?;
        refreshed.ensure_ready(scope)?;
        Ok(crate::auth::SessionBundle {
            subject: refreshed.subject,
            scope,
            expires_at: refreshed.auth_state.expires_at.clone(),
            refreshed: true,
            bearer_token: refreshed.auth_state.bearer_token,
        })
    }

    async fn refresh_session(&self) -> crate::ImResult<crate::auth::SessionUpdate> {
        let snapshot = self.snapshot_async().await?;
        snapshot.ensure_refresh_ready()?;
        let mut transport = crate::internal::transport::CoreHttpTransport::new(self.client);
        transport.refresh_jwt_async().await?;
        let refreshed = self.snapshot_async().await?;
        Ok(crate::auth::SessionUpdate {
            subject: snapshot.subject,
            previous_expires_at: snapshot.auth_state.expires_at.clone(),
            new_expires_at: refreshed.auth_state.expires_at,
            refreshed: true,
            bearer_token: refreshed.auth_state.bearer_token,
        })
    }

    async fn status(&self) -> crate::ImResult<crate::auth::AuthStatus> {
        let snapshot = self.snapshot_async().await?;
        Ok(crate::auth::AuthStatus {
            subject: snapshot.subject.clone(),
            has_session: snapshot.ready_for_auth
                && snapshot.did_document_available
                && snapshot.private_key_available
                && snapshot.auth_state.has_valid_token,
            expires_at: snapshot.auth_state.expires_at.clone(),
            needs_refresh: snapshot.ready_for_auth
                && snapshot.did_document_available
                && snapshot.private_key_available
                && (!snapshot.auth_state.has_token || snapshot.auth_state.needs_refresh),
            warnings: snapshot.warnings(),
        })
    }
}

impl<T> AsyncSessionProvider for &T
where
    T: AsyncSessionProvider + ?Sized,
{
    async fn ensure_session(
        &self,
        scope: crate::auth::AuthScope,
    ) -> crate::ImResult<crate::auth::SessionBundle> {
        (**self).ensure_session(scope).await
    }

    async fn refresh_session(&self) -> crate::ImResult<crate::auth::SessionUpdate> {
        (**self).refresh_session().await
    }

    async fn status(&self) -> crate::ImResult<crate::auth::AuthStatus> {
        (**self).status().await
    }
}

struct SessionSnapshot {
    subject: crate::ids::Did,
    ready_for_auth: bool,
    ready_for_messaging: bool,
    did_document_available: bool,
    private_key_available: bool,
    auth_state: AuthStateSnapshot,
}

impl SessionSnapshot {
    fn ensure_ready(&self, scope: crate::auth::AuthScope) -> crate::ImResult<()> {
        self.ensure_identity_ready(scope)?;
        if !self.auth_state.has_token {
            return Err(crate::ImError::AuthRequired);
        }
        if self.auth_state.token_expired {
            return Err(crate::ImError::SessionExpired);
        }
        Ok(())
    }

    fn ensure_identity_ready(&self, scope: crate::auth::AuthScope) -> crate::ImResult<()> {
        if !self.ready_for_auth {
            return Err(crate::ImError::AuthRequired);
        }
        if matches!(
            scope,
            crate::auth::AuthScope::Messaging | crate::auth::AuthScope::GroupMessaging
        ) && !self.ready_for_messaging
        {
            return Err(crate::ImError::IdentityNotReady {
                identity: self.subject.as_str().to_string(),
                missing: vec!["messaging_registration".to_string()],
            });
        }
        if !self.did_document_available {
            return Err(crate::ImError::CredentialFileUnreadable {
                path_kind: "did_document".to_string(),
                detail: "DID document is missing".to_string(),
            });
        }
        if !self.private_key_available {
            return Err(crate::ImError::CredentialFileUnreadable {
                path_kind: "private_key".to_string(),
                detail: "private key material is missing".to_string(),
            });
        }
        Ok(())
    }

    fn warnings(&self) -> Vec<String> {
        let mut warnings = Vec::new();
        if !self.did_document_available {
            warnings.push("did document is missing".to_string());
        }
        if !self.private_key_available {
            warnings.push("private key is missing".to_string());
        }
        if !self.auth_state.has_token {
            warnings.push("auth state has no JWT".to_string());
        } else if self.auth_state.token_expired {
            warnings.push("auth state JWT is expired".to_string());
        } else if self.auth_state.needs_refresh {
            warnings.push("auth state JWT expires soon".to_string());
        }
        warnings
    }

    fn ensure_refresh_ready(&self) -> crate::ImResult<()> {
        if !self.ready_for_auth {
            return Err(crate::ImError::AuthRequired);
        }
        if !self.did_document_available {
            return Err(crate::ImError::CredentialFileUnreadable {
                path_kind: "did_document".to_string(),
                detail: "DID document is missing".to_string(),
            });
        }
        if !self.private_key_available {
            return Err(crate::ImError::CredentialFileUnreadable {
                path_kind: "private_key".to_string(),
                detail: "private key material is missing".to_string(),
            });
        }
        Ok(())
    }
}