feat: async SunbeamClient factory with unified auth resolution

SunbeamClient accessors are now async and resolve auth per-client:
- SSO bearer (get_token) for admin APIs, Matrix, La Suite, OpenSearch
- Gitea PAT (get_gitea_token) for VCS
- None for Prometheus, Loki, S3, LiveKit

Fixes client URLs to match deployed routes: hydra→hydra.{domain},
matrix→messages.{domain}, grafana→metrics.{domain},
prometheus→systemmetrics.{domain}, loki→systemlogs.{domain}.

Removes all ad-hoc token helpers from CLI modules (matrix_with_token,
os_client, people_client, etc). Every dispatch just calls
client.service().await?.
This commit is contained in:
2026-03-22 18:57:22 +00:00
parent 34647e6bcb
commit faf525522c
17 changed files with 224 additions and 237 deletions

4
Cargo.lock generated
View File

@@ -3469,7 +3469,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sunbeam" name = "sunbeam"
version = "1.0.0" version = "1.0.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@@ -3482,7 +3482,7 @@ dependencies = [
[[package]] [[package]]
name = "sunbeam-sdk" name = "sunbeam-sdk"
version = "1.0.0" version = "1.0.1"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",

View File

@@ -29,9 +29,9 @@ impl ServiceClient for HydraClient {
} }
impl HydraClient { impl HydraClient {
/// Build a HydraClient from domain (e.g. `https://auth.{domain}`). /// Build a HydraClient from domain (e.g. `https://hydra.{domain}`).
pub fn connect(domain: &str) -> Self { pub fn connect(domain: &str) -> Self {
let base_url = format!("https://auth.{domain}"); let base_url = format!("https://hydra.{domain}");
Self::from_parts(base_url, AuthMethod::None) Self::from_parts(base_url, AuthMethod::None)
} }
@@ -467,7 +467,7 @@ mod tests {
#[test] #[test]
fn test_connect_url() { fn test_connect_url() {
let c = HydraClient::connect("sunbeam.pt"); let c = HydraClient::connect("sunbeam.pt");
assert_eq!(c.base_url(), "https://auth.sunbeam.pt"); assert_eq!(c.base_url(), "https://hydra.sunbeam.pt");
assert_eq!(c.service_name(), "hydra"); assert_eq!(c.service_name(), "hydra");
} }

View File

@@ -7,7 +7,7 @@
use crate::error::{Result, ResultExt, SunbeamError}; use crate::error::{Result, ResultExt, SunbeamError};
use reqwest::Method; use reqwest::Method;
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use std::sync::OnceLock; use tokio::sync::OnceCell;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// AuthMethod // AuthMethod
@@ -222,51 +222,51 @@ impl HttpTransport {
/// Unified entry point for all service clients. /// Unified entry point for all service clients.
/// ///
/// Lazily constructs and caches per-service clients from the active config /// Lazily constructs and caches per-service clients from the active config
/// context. Each accessor returns a `&Client` reference, constructing on /// context. Each accessor resolves auth and returns a `&Client` reference,
/// first call via [`OnceLock`]. /// constructing on first call via [`OnceCell`] (async-aware).
///
/// Auth is resolved per-client:
/// - SSO bearer (`get_token()`) — admin APIs, Matrix, La Suite, OpenSearch
/// - Gitea PAT (`get_gitea_token()`) — Gitea
/// - None — Prometheus, Loki, S3, LiveKit
pub struct SunbeamClient { pub struct SunbeamClient {
ctx: crate::config::Context, ctx: crate::config::Context,
domain: String, domain: String,
// Phase 1
#[cfg(feature = "identity")] #[cfg(feature = "identity")]
kratos: OnceLock<crate::identity::KratosClient>, kratos: OnceCell<crate::identity::KratosClient>,
#[cfg(feature = "identity")] #[cfg(feature = "identity")]
hydra: OnceLock<crate::auth::hydra::HydraClient>, hydra: OnceCell<crate::auth::hydra::HydraClient>,
// Phase 2
#[cfg(feature = "gitea")] #[cfg(feature = "gitea")]
gitea: OnceLock<crate::gitea::GiteaClient>, gitea: OnceCell<crate::gitea::GiteaClient>,
// Phase 3
#[cfg(feature = "matrix")] #[cfg(feature = "matrix")]
matrix: OnceLock<crate::matrix::MatrixClient>, matrix: OnceCell<crate::matrix::MatrixClient>,
#[cfg(feature = "opensearch")] #[cfg(feature = "opensearch")]
opensearch: OnceLock<crate::search::OpenSearchClient>, opensearch: OnceCell<crate::search::OpenSearchClient>,
#[cfg(feature = "s3")] #[cfg(feature = "s3")]
s3: OnceLock<crate::storage::S3Client>, s3: OnceCell<crate::storage::S3Client>,
#[cfg(feature = "livekit")] #[cfg(feature = "livekit")]
livekit: OnceLock<crate::media::LiveKitClient>, livekit: OnceCell<crate::media::LiveKitClient>,
#[cfg(feature = "monitoring")] #[cfg(feature = "monitoring")]
prometheus: OnceLock<crate::monitoring::PrometheusClient>, prometheus: OnceCell<crate::monitoring::PrometheusClient>,
#[cfg(feature = "monitoring")] #[cfg(feature = "monitoring")]
loki: OnceLock<crate::monitoring::LokiClient>, loki: OnceCell<crate::monitoring::LokiClient>,
#[cfg(feature = "monitoring")] #[cfg(feature = "monitoring")]
grafana: OnceLock<crate::monitoring::GrafanaClient>, grafana: OnceCell<crate::monitoring::GrafanaClient>,
// Phase 4
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
people: OnceLock<crate::lasuite::PeopleClient>, people: OnceCell<crate::lasuite::PeopleClient>,
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
docs: OnceLock<crate::lasuite::DocsClient>, docs: OnceCell<crate::lasuite::DocsClient>,
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
meet: OnceLock<crate::lasuite::MeetClient>, meet: OnceCell<crate::lasuite::MeetClient>,
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
drive: OnceLock<crate::lasuite::DriveClient>, drive: OnceCell<crate::lasuite::DriveClient>,
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
messages: OnceLock<crate::lasuite::MessagesClient>, messages: OnceCell<crate::lasuite::MessagesClient>,
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
calendars: OnceLock<crate::lasuite::CalendarsClient>, calendars: OnceCell<crate::lasuite::CalendarsClient>,
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
find: OnceLock<crate::lasuite::FindClient>, find: OnceCell<crate::lasuite::FindClient>,
// Bao/Planka stay in their existing modules bao: OnceCell<crate::openbao::BaoClient>,
bao: OnceLock<crate::openbao::BaoClient>,
} }
impl SunbeamClient { impl SunbeamClient {
@@ -276,40 +276,40 @@ impl SunbeamClient {
domain: ctx.domain.clone(), domain: ctx.domain.clone(),
ctx: ctx.clone(), ctx: ctx.clone(),
#[cfg(feature = "identity")] #[cfg(feature = "identity")]
kratos: OnceLock::new(), kratos: OnceCell::new(),
#[cfg(feature = "identity")] #[cfg(feature = "identity")]
hydra: OnceLock::new(), hydra: OnceCell::new(),
#[cfg(feature = "gitea")] #[cfg(feature = "gitea")]
gitea: OnceLock::new(), gitea: OnceCell::new(),
#[cfg(feature = "matrix")] #[cfg(feature = "matrix")]
matrix: OnceLock::new(), matrix: OnceCell::new(),
#[cfg(feature = "opensearch")] #[cfg(feature = "opensearch")]
opensearch: OnceLock::new(), opensearch: OnceCell::new(),
#[cfg(feature = "s3")] #[cfg(feature = "s3")]
s3: OnceLock::new(), s3: OnceCell::new(),
#[cfg(feature = "livekit")] #[cfg(feature = "livekit")]
livekit: OnceLock::new(), livekit: OnceCell::new(),
#[cfg(feature = "monitoring")] #[cfg(feature = "monitoring")]
prometheus: OnceLock::new(), prometheus: OnceCell::new(),
#[cfg(feature = "monitoring")] #[cfg(feature = "monitoring")]
loki: OnceLock::new(), loki: OnceCell::new(),
#[cfg(feature = "monitoring")] #[cfg(feature = "monitoring")]
grafana: OnceLock::new(), grafana: OnceCell::new(),
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
people: OnceLock::new(), people: OnceCell::new(),
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
docs: OnceLock::new(), docs: OnceCell::new(),
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
meet: OnceLock::new(), meet: OnceCell::new(),
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
drive: OnceLock::new(), drive: OnceCell::new(),
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
messages: OnceLock::new(), messages: OnceCell::new(),
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
calendars: OnceLock::new(), calendars: OnceCell::new(),
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
find: OnceLock::new(), find: OnceCell::new(),
bao: OnceLock::new(), bao: OnceCell::new(),
} }
} }
@@ -323,131 +323,172 @@ impl SunbeamClient {
&self.ctx &self.ctx
} }
// -- Lazy accessors (each feature-gated) -------------------------------- // -- Auth helpers --------------------------------------------------------
/// Get cached SSO bearer token (from `sunbeam auth sso`).
async fn sso_token(&self) -> Result<String> {
crate::auth::get_token().await
}
/// Get cached Gitea PAT (from `sunbeam auth git`).
fn gitea_token(&self) -> Result<String> {
crate::auth::get_gitea_token()
}
// -- Lazy async accessors (each feature-gated) ---------------------------
//
// Each accessor resolves the appropriate auth and constructs the client
// with from_parts(url, auth). Cached after first call.
#[cfg(feature = "identity")] #[cfg(feature = "identity")]
pub fn kratos(&self) -> &crate::identity::KratosClient { pub async fn kratos(&self) -> Result<&crate::identity::KratosClient> {
self.kratos.get_or_init(|| { self.kratos.get_or_try_init(|| async {
crate::identity::KratosClient::connect(&self.domain) let token = self.sso_token().await?;
}) let url = format!("https://id.{}", self.domain);
Ok(crate::identity::KratosClient::from_parts(url, AuthMethod::Bearer(token)))
}).await
} }
#[cfg(feature = "identity")] #[cfg(feature = "identity")]
pub fn hydra(&self) -> &crate::auth::hydra::HydraClient { pub async fn hydra(&self) -> Result<&crate::auth::hydra::HydraClient> {
self.hydra.get_or_init(|| { self.hydra.get_or_try_init(|| async {
crate::auth::hydra::HydraClient::connect(&self.domain) let token = self.sso_token().await?;
}) let url = format!("https://hydra.{}", self.domain);
Ok(crate::auth::hydra::HydraClient::from_parts(url, AuthMethod::Bearer(token)))
}).await
} }
#[cfg(feature = "gitea")] #[cfg(feature = "gitea")]
pub fn gitea(&self) -> &crate::gitea::GiteaClient { pub async fn gitea(&self) -> Result<&crate::gitea::GiteaClient> {
self.gitea.get_or_init(|| { self.gitea.get_or_try_init(|| async {
crate::gitea::GiteaClient::connect(&self.domain) let token = self.gitea_token()?;
}) let url = format!("https://src.{}/api/v1", self.domain);
Ok(crate::gitea::GiteaClient::from_parts(url, AuthMethod::Token(token)))
}).await
} }
#[cfg(feature = "matrix")] #[cfg(feature = "matrix")]
pub fn matrix(&self) -> &crate::matrix::MatrixClient { pub async fn matrix(&self) -> Result<&crate::matrix::MatrixClient> {
self.matrix.get_or_init(|| { self.matrix.get_or_try_init(|| async {
crate::matrix::MatrixClient::connect(&self.domain) let token = self.sso_token().await?;
}) let url = format!("https://messages.{}/_matrix", self.domain);
Ok(crate::matrix::MatrixClient::from_parts(url, AuthMethod::Bearer(token)))
}).await
} }
#[cfg(feature = "opensearch")] #[cfg(feature = "opensearch")]
pub fn opensearch(&self) -> &crate::search::OpenSearchClient { pub async fn opensearch(&self) -> Result<&crate::search::OpenSearchClient> {
self.opensearch.get_or_init(|| { self.opensearch.get_or_try_init(|| async {
crate::search::OpenSearchClient::connect(&self.domain) let token = self.sso_token().await?;
}) let url = format!("https://search.{}", self.domain);
Ok(crate::search::OpenSearchClient::from_parts(url, AuthMethod::Bearer(token)))
}).await
} }
#[cfg(feature = "s3")] #[cfg(feature = "s3")]
pub fn s3(&self) -> &crate::storage::S3Client { pub async fn s3(&self) -> Result<&crate::storage::S3Client> {
self.s3.get_or_init(|| { self.s3.get_or_try_init(|| async {
crate::storage::S3Client::connect(&self.domain) Ok(crate::storage::S3Client::connect(&self.domain))
}) }).await
} }
#[cfg(feature = "livekit")] #[cfg(feature = "livekit")]
pub fn livekit(&self) -> &crate::media::LiveKitClient { pub async fn livekit(&self) -> Result<&crate::media::LiveKitClient> {
self.livekit.get_or_init(|| { self.livekit.get_or_try_init(|| async {
crate::media::LiveKitClient::connect(&self.domain) Ok(crate::media::LiveKitClient::connect(&self.domain))
}) }).await
} }
#[cfg(feature = "monitoring")] #[cfg(feature = "monitoring")]
pub fn prometheus(&self) -> &crate::monitoring::PrometheusClient { pub async fn prometheus(&self) -> Result<&crate::monitoring::PrometheusClient> {
self.prometheus.get_or_init(|| { self.prometheus.get_or_try_init(|| async {
crate::monitoring::PrometheusClient::connect(&self.domain) Ok(crate::monitoring::PrometheusClient::connect(&self.domain))
}) }).await
} }
#[cfg(feature = "monitoring")] #[cfg(feature = "monitoring")]
pub fn loki(&self) -> &crate::monitoring::LokiClient { pub async fn loki(&self) -> Result<&crate::monitoring::LokiClient> {
self.loki.get_or_init(|| { self.loki.get_or_try_init(|| async {
crate::monitoring::LokiClient::connect(&self.domain) Ok(crate::monitoring::LokiClient::connect(&self.domain))
}) }).await
} }
#[cfg(feature = "monitoring")] #[cfg(feature = "monitoring")]
pub fn grafana(&self) -> &crate::monitoring::GrafanaClient { pub async fn grafana(&self) -> Result<&crate::monitoring::GrafanaClient> {
self.grafana.get_or_init(|| { self.grafana.get_or_try_init(|| async {
crate::monitoring::GrafanaClient::connect(&self.domain) Ok(crate::monitoring::GrafanaClient::connect(&self.domain))
}) }).await
} }
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
pub fn people(&self) -> &crate::lasuite::PeopleClient { pub async fn people(&self) -> Result<&crate::lasuite::PeopleClient> {
self.people.get_or_init(|| { self.people.get_or_try_init(|| async {
crate::lasuite::PeopleClient::connect(&self.domain) let token = self.sso_token().await?;
}) let url = format!("https://people.{}/api/v1.0", self.domain);
Ok(crate::lasuite::PeopleClient::from_parts(url, AuthMethod::Bearer(token)))
}).await
} }
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
pub fn docs(&self) -> &crate::lasuite::DocsClient { pub async fn docs(&self) -> Result<&crate::lasuite::DocsClient> {
self.docs.get_or_init(|| { self.docs.get_or_try_init(|| async {
crate::lasuite::DocsClient::connect(&self.domain) let token = self.sso_token().await?;
}) let url = format!("https://docs.{}/api/v1.0", self.domain);
Ok(crate::lasuite::DocsClient::from_parts(url, AuthMethod::Bearer(token)))
}).await
} }
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
pub fn meet(&self) -> &crate::lasuite::MeetClient { pub async fn meet(&self) -> Result<&crate::lasuite::MeetClient> {
self.meet.get_or_init(|| { self.meet.get_or_try_init(|| async {
crate::lasuite::MeetClient::connect(&self.domain) let token = self.sso_token().await?;
}) let url = format!("https://meet.{}/api/v1.0", self.domain);
Ok(crate::lasuite::MeetClient::from_parts(url, AuthMethod::Bearer(token)))
}).await
} }
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
pub fn drive(&self) -> &crate::lasuite::DriveClient { pub async fn drive(&self) -> Result<&crate::lasuite::DriveClient> {
self.drive.get_or_init(|| { self.drive.get_or_try_init(|| async {
crate::lasuite::DriveClient::connect(&self.domain) let token = self.sso_token().await?;
}) let url = format!("https://drive.{}/api/v1.0", self.domain);
Ok(crate::lasuite::DriveClient::from_parts(url, AuthMethod::Bearer(token)))
}).await
} }
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
pub fn messages(&self) -> &crate::lasuite::MessagesClient { pub async fn messages(&self) -> Result<&crate::lasuite::MessagesClient> {
self.messages.get_or_init(|| { self.messages.get_or_try_init(|| async {
crate::lasuite::MessagesClient::connect(&self.domain) let token = self.sso_token().await?;
}) let url = format!("https://mail.{}/api/v1.0", self.domain);
Ok(crate::lasuite::MessagesClient::from_parts(url, AuthMethod::Bearer(token)))
}).await
} }
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
pub fn calendars(&self) -> &crate::lasuite::CalendarsClient { pub async fn calendars(&self) -> Result<&crate::lasuite::CalendarsClient> {
self.calendars.get_or_init(|| { self.calendars.get_or_try_init(|| async {
crate::lasuite::CalendarsClient::connect(&self.domain) let token = self.sso_token().await?;
}) let url = format!("https://calendar.{}/api/v1.0", self.domain);
Ok(crate::lasuite::CalendarsClient::from_parts(url, AuthMethod::Bearer(token)))
}).await
} }
#[cfg(feature = "lasuite")] #[cfg(feature = "lasuite")]
pub fn find(&self) -> &crate::lasuite::FindClient { pub async fn find(&self) -> Result<&crate::lasuite::FindClient> {
self.find.get_or_init(|| { self.find.get_or_try_init(|| async {
crate::lasuite::FindClient::connect(&self.domain) let token = self.sso_token().await?;
}) let url = format!("https://find.{}/api/v1.0", self.domain);
Ok(crate::lasuite::FindClient::from_parts(url, AuthMethod::Bearer(token)))
}).await
} }
pub fn bao(&self, base_url: &str) -> &crate::openbao::BaoClient { pub async fn bao(&self) -> Result<&crate::openbao::BaoClient> {
self.bao.get_or_init(|| { self.bao.get_or_try_init(|| async {
crate::openbao::BaoClient::new(base_url) let token = self.sso_token().await?;
}) let url = format!("https://vault.{}", self.domain);
Ok(crate::openbao::BaoClient::with_token(&url, &token))
}).await
} }
} }

View File

@@ -2,9 +2,9 @@
use clap::Subcommand; use clap::Subcommand;
use crate::client::SunbeamClient;
use crate::error::{Result, SunbeamError}; use crate::error::{Result, SunbeamError};
use crate::gitea::types::*; use crate::gitea::types::*;
use crate::gitea::GiteaClient;
use crate::output::{render, render_list, read_json_input, OutputFormat}; use crate::output::{render, render_list, read_json_input, OutputFormat};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -435,7 +435,8 @@ fn notification_row(n: &Notification) -> Vec<String> {
// Dispatch // Dispatch
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
pub async fn dispatch(cmd: VcsCommand, client: &GiteaClient, fmt: OutputFormat) -> Result<()> { pub async fn dispatch(cmd: VcsCommand, client: &SunbeamClient, fmt: OutputFormat) -> Result<()> {
let client = client.gitea().await?;
match cmd { match cmd {
// -- Repo ----------------------------------------------------------- // -- Repo -----------------------------------------------------------
VcsCommand::Repo { action } => match action { VcsCommand::Repo { action } => match action {

View File

@@ -349,7 +349,7 @@ pub async fn dispatch(
AuthCommand::Courier { action } => dispatch_courier(action, client, output).await, AuthCommand::Courier { action } => dispatch_courier(action, client, output).await,
// -- Kratos: Health ----------------------------------------------------- // -- Kratos: Health -----------------------------------------------------
AuthCommand::Health => { AuthCommand::Health => {
let status = client.kratos().alive().await?; let status = client.kratos().await?.alive().await?;
output::render(&status, output) output::render(&status, output)
} }
// -- Hydra: Client ------------------------------------------------------ // -- Hydra: Client ------------------------------------------------------
@@ -384,7 +384,7 @@ async fn dispatch_identity(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let kratos = client.kratos(); let kratos = client.kratos().await?;
match action { match action {
IdentityAction::List { page, page_size } => { IdentityAction::List { page, page_size } => {
let items = kratos.list_identities(page, page_size).await?; let items = kratos.list_identities(page, page_size).await?;
@@ -437,7 +437,7 @@ async fn dispatch_session(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let kratos = client.kratos(); let kratos = client.kratos().await?;
match action { match action {
SessionAction::List { SessionAction::List {
page_size, page_size,
@@ -486,7 +486,7 @@ async fn dispatch_recovery(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let kratos = client.kratos(); let kratos = client.kratos().await?;
match action { match action {
RecoveryAction::CreateCode { id, expires_in } => { RecoveryAction::CreateCode { id, expires_in } => {
let item = kratos let item = kratos
@@ -512,7 +512,7 @@ async fn dispatch_schema(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let kratos = client.kratos(); let kratos = client.kratos().await?;
match action { match action {
SchemaAction::List => { SchemaAction::List => {
let items = kratos.list_schemas().await?; let items = kratos.list_schemas().await?;
@@ -539,7 +539,7 @@ async fn dispatch_courier(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let kratos = client.kratos(); let kratos = client.kratos().await?;
match action { match action {
CourierAction::List { CourierAction::List {
page_size, page_size,
@@ -579,7 +579,7 @@ async fn dispatch_client(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let hydra = client.hydra(); let hydra = client.hydra().await?;
match action { match action {
ClientAction::List { limit, offset } => { ClientAction::List { limit, offset } => {
let items = hydra.list_clients(limit, offset).await?; let items = hydra.list_clients(limit, offset).await?;
@@ -631,7 +631,7 @@ async fn dispatch_jwk(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let hydra = client.hydra(); let hydra = client.hydra().await?;
match action { match action {
JwkAction::List { set_name } => { JwkAction::List { set_name } => {
let item = hydra.get_jwk_set(&set_name).await?; let item = hydra.get_jwk_set(&set_name).await?;
@@ -665,7 +665,7 @@ async fn dispatch_issuer(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let hydra = client.hydra(); let hydra = client.hydra().await?;
match action { match action {
IssuerAction::List => { IssuerAction::List => {
let items = hydra.list_trusted_issuers().await?; let items = hydra.list_trusted_issuers().await?;
@@ -711,7 +711,7 @@ async fn dispatch_token(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let hydra = client.hydra(); let hydra = client.hydra().await?;
match action { match action {
TokenAction::Introspect { token } => { TokenAction::Introspect { token } => {
let item = hydra.introspect_token(&token).await?; let item = hydra.introspect_token(&token).await?;

View File

@@ -6,44 +6,6 @@ use crate::client::SunbeamClient;
use crate::error::Result; use crate::error::Result;
use crate::output::{self, OutputFormat}; use crate::output::{self, OutputFormat};
// ═══════════════════════════════════════════════════════════════════════════
// Helper: build an authenticated La Suite client
// ═══════════════════════════════════════════════════════════════════════════
async fn people_client(domain: &str) -> Result<super::PeopleClient> {
let token = crate::auth::get_token().await?;
Ok(super::PeopleClient::connect(domain).with_token(&token))
}
async fn docs_client(domain: &str) -> Result<super::DocsClient> {
let token = crate::auth::get_token().await?;
Ok(super::DocsClient::connect(domain).with_token(&token))
}
async fn meet_client(domain: &str) -> Result<super::MeetClient> {
let token = crate::auth::get_token().await?;
Ok(super::MeetClient::connect(domain).with_token(&token))
}
async fn drive_client(domain: &str) -> Result<super::DriveClient> {
let token = crate::auth::get_token().await?;
Ok(super::DriveClient::connect(domain).with_token(&token))
}
async fn messages_client(domain: &str) -> Result<super::MessagesClient> {
let token = crate::auth::get_token().await?;
Ok(super::MessagesClient::connect(domain).with_token(&token))
}
async fn calendars_client(domain: &str) -> Result<super::CalendarsClient> {
let token = crate::auth::get_token().await?;
Ok(super::CalendarsClient::connect(domain).with_token(&token))
}
async fn find_client(domain: &str) -> Result<super::FindClient> {
let token = crate::auth::get_token().await?;
Ok(super::FindClient::connect(domain).with_token(&token))
}
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// People // People
@@ -143,7 +105,7 @@ pub async fn dispatch_people(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let people = people_client(client.domain()).await?; let people = client.people().await?;
match cmd { match cmd {
PeopleCommand::Contact { action } => match action { PeopleCommand::Contact { action } => match action {
ContactAction::List { page } => { ContactAction::List { page } => {
@@ -346,7 +308,7 @@ pub async fn dispatch_docs(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let docs = docs_client(client.domain()).await?; let docs = client.docs().await?;
match cmd { match cmd {
DocsCommand::Document { action } => match action { DocsCommand::Document { action } => match action {
DocumentAction::List { page } => { DocumentAction::List { page } => {
@@ -498,7 +460,7 @@ pub async fn dispatch_meet(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let meet = meet_client(client.domain()).await?; let meet = client.meet().await?;
match cmd { match cmd {
MeetCommand::Room { action } => match action { MeetCommand::Room { action } => match action {
RoomAction::List { page } => { RoomAction::List { page } => {
@@ -645,7 +607,7 @@ pub async fn dispatch_drive(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let drive = drive_client(client.domain()).await?; let drive = client.drive().await?;
match cmd { match cmd {
DriveCommand::File { action } => match action { DriveCommand::File { action } => match action {
FileAction::List { page } => { FileAction::List { page } => {
@@ -823,7 +785,7 @@ pub async fn dispatch_mail(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let mail = messages_client(client.domain()).await?; let mail = client.messages().await?;
match cmd { match cmd {
MailCommand::Mailbox { action } => match action { MailCommand::Mailbox { action } => match action {
MailboxAction::List => { MailboxAction::List => {
@@ -1013,7 +975,7 @@ pub async fn dispatch_cal(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let cal = calendars_client(client.domain()).await?; let cal = client.calendars().await?;
match cmd { match cmd {
CalCommand::Calendar { action } => match action { CalCommand::Calendar { action } => match action {
CalendarAction::List => { CalendarAction::List => {
@@ -1124,7 +1086,7 @@ pub async fn dispatch_find(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let find = find_client(client.domain()).await?; let find = client.find().await?;
match cmd { match cmd {
FindCommand::Search { query, page } => { FindCommand::Search { query, page } => {
let page_data = find.search(&query, page).await?; let page_data = find.search(&query, page).await?;

View File

@@ -1,22 +1,10 @@
//! CLI dispatch for Matrix chat commands. //! CLI dispatch for Matrix chat commands.
use crate::client::SunbeamClient;
use crate::error::Result; use crate::error::Result;
use crate::output::{self, OutputFormat}; use crate::output::{self, OutputFormat};
use clap::Subcommand; use clap::Subcommand;
// ---------------------------------------------------------------------------
// Auth helper
// ---------------------------------------------------------------------------
/// Construct a [`MatrixClient`] with a valid access token from the credential
/// cache. Fails if the user is not logged in.
async fn matrix_with_token(domain: &str) -> Result<super::MatrixClient> {
let token = crate::auth::get_token().await?;
let mut m = super::MatrixClient::connect(domain);
m.set_token(&token);
Ok(m)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Command tree // Command tree
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -343,8 +331,8 @@ pub enum UserAction {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Dispatch a parsed [`ChatCommand`] against the Matrix homeserver. /// Dispatch a parsed [`ChatCommand`] against the Matrix homeserver.
pub async fn dispatch(domain: &str, format: OutputFormat, cmd: ChatCommand) -> Result<()> { pub async fn dispatch(client: &SunbeamClient, format: OutputFormat, cmd: ChatCommand) -> Result<()> {
let m = matrix_with_token(domain).await?; let m = client.matrix().await?;
match cmd { match cmd {
// -- Whoami --------------------------------------------------------- // -- Whoami ---------------------------------------------------------

View File

@@ -32,9 +32,9 @@ impl ServiceClient for MatrixClient {
} }
impl MatrixClient { impl MatrixClient {
/// Build a MatrixClient from domain (e.g. `https://matrix.{domain}/_matrix`). /// Build a MatrixClient from domain (e.g. `https://messages.{domain}/_matrix`).
pub fn connect(domain: &str) -> Self { pub fn connect(domain: &str) -> Self {
let base_url = format!("https://matrix.{domain}/_matrix"); let base_url = format!("https://messages.{domain}/_matrix");
Self::from_parts(base_url, AuthMethod::Bearer(String::new())) Self::from_parts(base_url, AuthMethod::Bearer(String::new()))
} }
@@ -1204,7 +1204,7 @@ mod tests {
#[test] #[test]
fn test_connect_url() { fn test_connect_url() {
let c = MatrixClient::connect("sunbeam.pt"); let c = MatrixClient::connect("sunbeam.pt");
assert_eq!(c.base_url(), "https://matrix.sunbeam.pt/_matrix"); assert_eq!(c.base_url(), "https://messages.sunbeam.pt/_matrix");
assert_eq!(c.service_name(), "matrix"); assert_eq!(c.service_name(), "matrix");
} }

View File

@@ -177,7 +177,7 @@ async fn dispatch_room(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let lk = client.livekit(); let lk = client.livekit().await?;
match action { match action {
RoomAction::List => { RoomAction::List => {
let resp = lk.list_rooms().await?; let resp = lk.list_rooms().await?;
@@ -227,7 +227,7 @@ async fn dispatch_participant(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let lk = client.livekit(); let lk = client.livekit().await?;
match action { match action {
ParticipantAction::List { room } => { ParticipantAction::List { room } => {
let resp = lk let resp = lk
@@ -278,7 +278,7 @@ async fn dispatch_egress(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let lk = client.livekit(); let lk = client.livekit().await?;
match action { match action {
EgressAction::List { room } => { EgressAction::List { room } => {
let resp = lk let resp = lk

View File

@@ -425,7 +425,7 @@ async fn dispatch_prometheus(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let prom = client.prometheus(); let prom = client.prometheus().await?;
match action { match action {
PrometheusAction::Query { query, time } => { PrometheusAction::Query { query, time } => {
let res = prom.query(&query, time.as_deref()).await?; let res = prom.query(&query, time.as_deref()).await?;
@@ -511,7 +511,7 @@ async fn dispatch_loki(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let loki = client.loki(); let loki = client.loki().await?;
match action { match action {
LokiAction::Query { query, limit, time } => { LokiAction::Query { query, limit, time } => {
let res = loki.query(&query, limit, time.as_deref()).await?; let res = loki.query(&query, limit, time.as_deref()).await?;
@@ -631,7 +631,7 @@ async fn dispatch_grafana_dashboard(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let grafana = client.grafana(); let grafana = client.grafana().await?;
match action { match action {
GrafanaDashboardAction::List => { GrafanaDashboardAction::List => {
let items = grafana.list_dashboards().await?; let items = grafana.list_dashboards().await?;
@@ -696,7 +696,7 @@ async fn dispatch_grafana_datasource(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let grafana = client.grafana(); let grafana = client.grafana().await?;
match action { match action {
GrafanaDatasourceAction::List => { GrafanaDatasourceAction::List => {
let items = grafana.list_datasources().await?; let items = grafana.list_datasources().await?;
@@ -746,7 +746,7 @@ async fn dispatch_grafana_folder(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let grafana = client.grafana(); let grafana = client.grafana().await?;
match action { match action {
GrafanaFolderAction::List => { GrafanaFolderAction::List => {
let items = grafana.list_folders().await?; let items = grafana.list_folders().await?;
@@ -794,7 +794,7 @@ async fn dispatch_grafana_annotation(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let grafana = client.grafana(); let grafana = client.grafana().await?;
match action { match action {
GrafanaAnnotationAction::List { params } => { GrafanaAnnotationAction::List { params } => {
let items = grafana.list_annotations(params.as_deref()).await?; let items = grafana.list_annotations(params.as_deref()).await?;
@@ -833,7 +833,7 @@ async fn dispatch_grafana_alert(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let grafana = client.grafana(); let grafana = client.grafana().await?;
match action { match action {
GrafanaAlertAction::List => { GrafanaAlertAction::List => {
let items = grafana.get_alert_rules().await?; let items = grafana.get_alert_rules().await?;
@@ -879,7 +879,7 @@ async fn dispatch_grafana_org(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let grafana = client.grafana(); let grafana = client.grafana().await?;
match action { match action {
GrafanaOrgAction::Get => { GrafanaOrgAction::Get => {
let item = grafana.get_current_org().await?; let item = grafana.get_current_org().await?;

View File

@@ -27,9 +27,9 @@ impl ServiceClient for GrafanaClient {
} }
impl GrafanaClient { impl GrafanaClient {
/// Build a GrafanaClient from domain (e.g. `https://grafana.{domain}/api`). /// Build a GrafanaClient from domain (e.g. `https://metrics.{domain}/api`).
pub fn connect(domain: &str) -> Self { pub fn connect(domain: &str) -> Self {
let base_url = format!("https://grafana.{domain}/api"); let base_url = format!("https://metrics.{domain}/api");
Self::from_parts(base_url, AuthMethod::None) Self::from_parts(base_url, AuthMethod::None)
} }
@@ -410,7 +410,7 @@ mod tests {
#[test] #[test]
fn test_connect_url() { fn test_connect_url() {
let c = GrafanaClient::connect("sunbeam.pt"); let c = GrafanaClient::connect("sunbeam.pt");
assert_eq!(c.base_url(), "https://grafana.sunbeam.pt/api"); assert_eq!(c.base_url(), "https://metrics.sunbeam.pt/api");
assert_eq!(c.service_name(), "grafana"); assert_eq!(c.service_name(), "grafana");
} }

View File

@@ -27,9 +27,9 @@ impl ServiceClient for LokiClient {
} }
impl LokiClient { impl LokiClient {
/// Build a LokiClient from domain (e.g. `https://loki.{domain}/loki/api/v1`). /// Build a LokiClient from domain (e.g. `https://systemlogs.{domain}/loki/api/v1`).
pub fn connect(domain: &str) -> Self { pub fn connect(domain: &str) -> Self {
let base_url = format!("https://loki.{domain}/loki/api/v1"); let base_url = format!("https://systemlogs.{domain}/loki/api/v1");
Self::from_parts(base_url, AuthMethod::None) Self::from_parts(base_url, AuthMethod::None)
} }
@@ -254,7 +254,7 @@ mod tests {
#[test] #[test]
fn test_connect_url() { fn test_connect_url() {
let c = LokiClient::connect("sunbeam.pt"); let c = LokiClient::connect("sunbeam.pt");
assert_eq!(c.base_url(), "https://loki.sunbeam.pt/loki/api/v1"); assert_eq!(c.base_url(), "https://systemlogs.sunbeam.pt/loki/api/v1");
assert_eq!(c.service_name(), "loki"); assert_eq!(c.service_name(), "loki");
} }

View File

@@ -27,9 +27,9 @@ impl ServiceClient for PrometheusClient {
} }
impl PrometheusClient { impl PrometheusClient {
/// Build a PrometheusClient from domain (e.g. `https://prometheus.{domain}/api/v1`). /// Build a PrometheusClient from domain (e.g. `https://systemmetrics.{domain}/api/v1`).
pub fn connect(domain: &str) -> Self { pub fn connect(domain: &str) -> Self {
let base_url = format!("https://prometheus.{domain}/api/v1"); let base_url = format!("https://systemmetrics.{domain}/api/v1");
Self::from_parts(base_url, AuthMethod::None) Self::from_parts(base_url, AuthMethod::None)
} }
@@ -253,7 +253,7 @@ mod tests {
#[test] #[test]
fn test_connect_url() { fn test_connect_url() {
let c = PrometheusClient::connect("sunbeam.pt"); let c = PrometheusClient::connect("sunbeam.pt");
assert_eq!(c.base_url(), "https://prometheus.sunbeam.pt/api/v1"); assert_eq!(c.base_url(), "https://systemmetrics.sunbeam.pt/api/v1");
assert_eq!(c.service_name(), "prometheus"); assert_eq!(c.service_name(), "prometheus");
} }

View File

@@ -4,6 +4,7 @@ use std::collections::HashMap;
use clap::Subcommand; use clap::Subcommand;
use crate::client::SunbeamClient;
use crate::error::Result; use crate::error::Result;
use crate::output::{self, OutputFormat}; use crate::output::{self, OutputFormat};
@@ -226,9 +227,10 @@ fn read_text_input(flag: Option<&str>) -> Result<String> {
pub async fn dispatch( pub async fn dispatch(
cmd: VaultCommand, cmd: VaultCommand,
bao: &super::BaoClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let bao = client.bao().await?;
match cmd { match cmd {
// -- Status --------------------------------------------------------- // -- Status ---------------------------------------------------------
VaultCommand::Status => { VaultCommand::Status => {

View File

@@ -6,17 +6,6 @@ use serde_json::json;
use crate::error::Result; use crate::error::Result;
use crate::output::{self, OutputFormat}; use crate::output::{self, OutputFormat};
// ---------------------------------------------------------------------------
// Client helper
// ---------------------------------------------------------------------------
async fn os_client(domain: &str) -> Result<super::OpenSearchClient> {
let token = crate::auth::get_token().await?;
let mut c = super::OpenSearchClient::connect(domain);
c.set_token(token);
Ok(c)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Top-level command enum // Top-level command enum
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -413,7 +402,7 @@ pub async fn dispatch(
client: &crate::client::SunbeamClient, client: &crate::client::SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let c = os_client(client.domain()).await?; let c = client.opensearch().await?;
match cmd { match cmd {
// ----------------------------------------------------------------- // -----------------------------------------------------------------

View File

@@ -152,7 +152,7 @@ async fn dispatch_bucket(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let s3 = client.s3(); let s3 = client.s3().await?;
match action { match action {
BucketAction::List => { BucketAction::List => {
let resp = s3.list_buckets().await?; let resp = s3.list_buckets().await?;
@@ -194,7 +194,7 @@ async fn dispatch_object(
client: &SunbeamClient, client: &SunbeamClient,
fmt: OutputFormat, fmt: OutputFormat,
) -> Result<()> { ) -> Result<()> {
let s3 = client.s3(); let s3 = client.s3().await?;
match action { match action {
ObjectAction::List { ObjectAction::List {
bucket, bucket,

View File

@@ -927,12 +927,14 @@ pub async fn dispatch() -> Result<()> {
let sc = sunbeam_sdk::client::SunbeamClient::from_context( let sc = sunbeam_sdk::client::SunbeamClient::from_context(
&sunbeam_sdk::config::active_context(), &sunbeam_sdk::config::active_context(),
); );
sunbeam_sdk::gitea::cli::dispatch(action, sc.gitea(), cli.output_format).await sunbeam_sdk::gitea::cli::dispatch(action, &sc, cli.output_format).await
} }
Some(Verb::Chat { action }) => { Some(Verb::Chat { action }) => {
let domain = sunbeam_sdk::config::active_context().domain.clone(); let sc = sunbeam_sdk::client::SunbeamClient::from_context(
sunbeam_sdk::matrix::cli::dispatch(&domain, cli.output_format, action).await &sunbeam_sdk::config::active_context(),
);
sunbeam_sdk::matrix::cli::dispatch(&sc, cli.output_format, action).await
} }
Some(Verb::Search { action }) => { Some(Verb::Search { action }) => {
@@ -964,8 +966,10 @@ pub async fn dispatch() -> Result<()> {
} }
Some(Verb::Vault { action }) => { Some(Verb::Vault { action }) => {
let bao = sunbeam_sdk::openbao::BaoClient::new("http://127.0.0.1:8200"); let sc = sunbeam_sdk::client::SunbeamClient::from_context(
sunbeam_sdk::openbao::cli::dispatch(action, &bao, cli.output_format).await &sunbeam_sdk::config::active_context(),
);
sunbeam_sdk::openbao::cli::dispatch(action, &sc, cli.output_format).await
} }
Some(Verb::People { action }) => { Some(Verb::People { action }) => {