use crate::{CliEnvironment, LoginProvider, RuntimeError};
mod identity;
mod login;
mod logout;
mod session;
mod store;
use session::AuthSource;
const AUTHENTICATED_STATUS_NEXT: &str =
"run logbrew releases or logbrew logs --release <release> --environment <environment>";
fn normalized_api_base(base_url: &str) -> Result<String, RuntimeError> {
let mut url = reqwest::Url::parse(base_url).map_err(|_| invalid_api_url())?;
if !matches!(url.scheme(), "http" | "https")
|| url.host_str().is_none()
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
{
return Err(invalid_api_url());
}
let normalized_path = url.path().trim_end_matches('/').to_owned();
url.set_path(normalized_path.as_str());
Ok(url.as_str().trim_end_matches('/').to_owned())
}
pub(crate) fn token_is_project_ingest_key(token: Option<&str>) -> bool {
token.is_some_and(|token| token.trim_start().starts_with("lbw_ingest_"))
}
const fn invalid_api_url() -> RuntimeError {
RuntimeError::Unavailable {
message: "the configured API URL is invalid",
next: "set LOGBREW_API_URL to an http or https API base and retry",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct AuthSnapshot {
pub(crate) authenticated: bool,
pub(crate) source: &'static str,
pub(crate) label: &'static str,
pub(crate) next: &'static str,
}
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct AuthCredential {
token: String,
source: &'static str,
label: &'static str,
refreshable: bool,
}
impl AuthCredential {
pub(crate) const fn token(&self) -> &str {
self.token.as_str()
}
pub(crate) const fn source(&self) -> &'static str {
self.source
}
pub(crate) const fn label(&self) -> &'static str {
self.label
}
pub(crate) fn redact_response_body(&self, body: &str) -> String {
body.replace(self.token.as_str(), "[redacted]")
}
}
impl std::fmt::Debug for AuthCredential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthCredential")
.field("token", &"[redacted]")
.field("source", &self.source)
.field("label", &self.label)
.field("refreshable", &self.refreshable)
.finish()
}
}
pub(crate) async fn execute_login<W: std::io::Write>(
env: &CliEnvironment,
provider: LoginProvider,
should_open_browser: bool,
json: bool,
output: &mut W,
) -> Result<(), RuntimeError> {
login::execute_login(env, provider, should_open_browser, json, output).await
}
pub(crate) async fn execute_whoami<W: std::io::Write>(
env: &CliEnvironment,
json: bool,
output: &mut W,
) -> Result<(), RuntimeError> {
identity::execute(env, json, output).await
}
pub(crate) async fn execute_logout<W: std::io::Write>(
env: &CliEnvironment,
json: bool,
output: &mut W,
) -> Result<(), RuntimeError> {
logout::execute(env, json, output).await
}
pub(crate) async fn send_authenticated_with_refresh<F>(
client: &reqwest::Client,
env: &CliEnvironment,
build_request: F,
) -> Result<(reqwest::Response, AuthCredential), RuntimeError>
where
F: Fn(&reqwest::Client, &AuthCredential) -> reqwest::RequestBuilder,
{
session::send_authenticated_with_refresh(client, env, build_request).await
}
pub(crate) async fn send_account_authenticated_with_refresh<F>(
client: &reqwest::Client,
env: &CliEnvironment,
build_request: F,
) -> Result<(reqwest::Response, AuthCredential), RuntimeError>
where
F: Fn(&reqwest::Client, &AuthCredential) -> reqwest::RequestBuilder,
{
session::send_account_authenticated_with_refresh(client, env, build_request).await
}
pub(crate) async fn send_authenticated_without_refresh<F>(
client: &reqwest::Client,
env: &CliEnvironment,
build_request: F,
) -> Result<(reqwest::Response, AuthCredential), RuntimeError>
where
F: Fn(&reqwest::Client, &AuthCredential) -> reqwest::RequestBuilder,
{
session::send_authenticated_without_refresh(client, env, build_request).await
}
pub(crate) fn write_status_success_with_auth_snapshot<W: std::io::Write>(
env: &CliEnvironment,
status_code: u16,
body: &str,
json: bool,
output: &mut W,
auth_status: &AuthSnapshot,
) -> Result<(), RuntimeError> {
if json {
let mut response = serde_json::json!({
"ok": true,
"status": "reachable",
"status_code": status_code,
"body": body,
"api_url": env.base_url,
"authenticated": auth_status.authenticated,
"auth_source": auth_status.source,
"next": auth_status.next,
});
if auth_status.authenticated {
if let Some(object) = response.as_object_mut() {
drop(object.insert("agent_use".to_owned(), authenticated_agent_use()));
}
}
writeln!(output, "{response}")?;
} else {
writeln!(output, "LogBrew API reachable.")?;
writeln!(output, "API: {}", env.base_url)?;
writeln!(output, "Auth: {}", auth_status.label)?;
if auth_status.authenticated {
write_authenticated_agent_use_prompt(output)?;
}
writeln!(output, "Next: {}", auth_status.next)?;
}
Ok(())
}
fn authenticated_agent_use() -> serde_json::Value {
serde_json::json!({
"prompt": "How should your AI use LogBrew?",
"default": "on_request",
"options": [
{
"id": "on_request",
"label": "Check only when requested",
"token_use": "lower",
"available": true,
"description": "Your AI runs LogBrew commands when you ask."
},
{
"id": "keep_watching",
"label": "Keep watching this session",
"token_use": "higher",
"available": true,
"description": "Your AI watches new events/logs until stopped.",
"command": "logbrew watch --json"
},
{
"id": "watch_errors_critical",
"label": "Watch only errors and critical issues",
"token_use": "moderate",
"available": true,
"description": "Your AI ignores lower-severity logs/events.",
"command": "logbrew watch --severity error,critical --json"
}
]
})
}
fn write_authenticated_agent_use_prompt<W: std::io::Write>(output: &mut W) -> std::io::Result<()> {
writeln!(output, "LogBrew is connected. How should your AI use it?")?;
writeln!(output)?;
writeln!(output, "1. Check only when requested")?;
writeln!(
output,
" Lower token use. Your AI runs LogBrew commands when you ask."
)?;
writeln!(output)?;
writeln!(output, "2. Keep watching this session")?;
writeln!(
output,
" Higher token use. Your AI watches new events/logs until stopped."
)?;
writeln!(output, " Command: logbrew watch --json")?;
writeln!(output)?;
writeln!(output, "3. Watch only errors and critical issues")?;
writeln!(
output,
" Moderate token use. Your AI ignores lower-severity logs/events."
)?;
writeln!(
output,
" Command: logbrew watch --severity error,critical --json"
)?;
writeln!(output)?;
Ok(())
}
pub(crate) fn inspect_auth_snapshot(env: &CliEnvironment) -> Result<AuthSnapshot, RuntimeError> {
let status = inspect_auth_status(env)?;
Ok(AuthSnapshot {
authenticated: status.is_authenticated(),
source: status.source_key(),
label: status.human_label(),
next: status.next_step(),
})
}
fn inspect_auth_status(env: &CliEnvironment) -> Result<AuthStatus, RuntimeError> {
if env
.token
.as_ref()
.is_some_and(|token| !token.trim().is_empty())
{
return Ok(AuthStatus::Configured(AuthSource::Env));
}
let Some(home) = &env.home else {
return Ok(AuthStatus::Missing);
};
let origin = normalized_api_base(env.base_url.as_str())?;
match store::read_access_token(home, origin.as_str()) {
Ok(_) => Ok(AuthStatus::Configured(AuthSource::TokenFile)),
Err(RuntimeError::MissingToken) => Ok(AuthStatus::Missing),
Err(error) => Err(error),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AuthStatus {
Configured(AuthSource),
Missing,
}
impl AuthStatus {
const fn is_authenticated(self) -> bool {
matches!(self, Self::Configured(_))
}
const fn source_key(self) -> &'static str {
match self {
Self::Configured(source) => source.key(),
Self::Missing => "missing",
}
}
const fn human_label(self) -> &'static str {
match self {
Self::Configured(source) => source.human_label(),
Self::Missing => "not logged in",
}
}
const fn next_step(self) -> &'static str {
match self {
Self::Configured(_) => AUTHENTICATED_STATUS_NEXT,
Self::Missing => "run logbrew login",
}
}
}