diff --git a/apps/fabro-web/app/routes/auth-login.tsx b/apps/fabro-web/app/routes/auth-login.tsx
index 69ec23b57..be4d4d3c4 100644
--- a/apps/fabro-web/app/routes/auth-login.tsx
+++ b/apps/fabro-web/app/routes/auth-login.tsx
@@ -113,8 +113,7 @@ function DevTokenForm({
{showLocation ? (
- Paste the dev token from your terminal or{" "}
- cat ~/.fabro/dev-token.
+ Paste the dev token from your server terminal or install output.
) : null}
diff --git a/lib/crates/fabro-cli/src/args.rs b/lib/crates/fabro-cli/src/args.rs
index 54d651231..756e8757f 100644
--- a/lib/crates/fabro-cli/src/args.rs
+++ b/lib/crates/fabro-cli/src/args.rs
@@ -1452,6 +1452,10 @@ pub(crate) struct AuthLoginArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
+ /// Log in with a dev-token instead of browser OAuth
+ #[arg(long, conflicts_with_all = ["no_browser", "timeout"])]
+ pub(crate) dev_token: Option,
+
/// Print the browser URL instead of opening it automatically
#[arg(long)]
pub(crate) no_browser: bool,
diff --git a/lib/crates/fabro-cli/src/commands/auth/login.rs b/lib/crates/fabro-cli/src/commands/auth/login.rs
index af2b68010..9c582fdd1 100644
--- a/lib/crates/fabro-cli/src/commands/auth/login.rs
+++ b/lib/crates/fabro-cli/src/commands/auth/login.rs
@@ -2,9 +2,10 @@ use std::time::Duration;
use anyhow::{Context as _, Result, bail};
use chrono::{DateTime, Utc};
-use fabro_client::{AuthEntry, AuthStore, StoredSubject};
+use fabro_client::{AuthEntry, AuthStore, DevTokenEntry, OAuthEntry, StoredSubject};
use fabro_http::header::CONTENT_TYPE;
use fabro_util::browser;
+use fabro_util::dev_token::validate_dev_token_format;
use fabro_util::printer::Printer;
use serde::Deserialize;
use tokio::time::timeout;
@@ -36,6 +37,22 @@ pub(super) async fn login_command(args: AuthLoginArgs, base_ctx: &CommandContext
base_ctx.require_no_json_override()?;
let printer = base_ctx.printer();
+ if let Some(token) = args.dev_token.as_ref() {
+ if !validate_dev_token_format(token) {
+ bail!("invalid dev-token format");
+ }
+ let target = user_config::resolve_server_target(&args.server, base_ctx.user_settings())?;
+ AuthStore::default().put(
+ &target,
+ AuthEntry::DevToken(DevTokenEntry {
+ token: token.clone(),
+ logged_in_at: Utc::now(),
+ }),
+ )?;
+ fabro_util::printerr!(printer, "Logged in to {} with dev-token", target);
+ return Ok(());
+ }
+
#[cfg(not(unix))]
{
let _ = (args, printer);
@@ -80,7 +97,7 @@ pub(super) async fn login_command(args: AuthLoginArgs, base_ctx: &CommandContext
};
let tokens = exchange_cli_token(&target, &code, &pkce.verifier, &redirect_uri).await?;
- let entry = AuthEntry {
+ let entry = OAuthEntry {
access_token: tokens.access_token,
access_token_expires_at: tokens.access_token_expires_at,
refresh_token: tokens.refresh_token,
@@ -95,7 +112,7 @@ pub(super) async fn login_command(args: AuthLoginArgs, base_ctx: &CommandContext
logged_in_at: Utc::now(),
};
let summary = identity_summary(&entry.subject);
- AuthStore::default().put(&target, entry)?;
+ AuthStore::default().put(&target, AuthEntry::OAuth(entry))?;
fabro_util::printerr!(printer, "Logged in to {} as {}", target, summary);
Ok(())
}
@@ -199,7 +216,7 @@ fn login_failure_message(error_code: &str, error_description: Option<&str>) -> S
"GitHub session required. Complete sign-in in the browser and try again.".to_string()
}
"github_not_configured" => {
- "The fabro server does not have GitHub login enabled. Ask the operator to enable it or use a dev-token.".to_string()
+ "This server uses dev-token auth. Copy the token from the server and run: `fabro auth login --dev-token `".to_string()
}
"access_denied" => "Authorization denied.".to_string(),
"unauthorized" => "Login not permitted.".to_string(),
@@ -280,7 +297,7 @@ mod tests {
"github_not_configured",
Some("GitHub authentication is not enabled on this server")
),
- "The fabro server does not have GitHub login enabled. Ask the operator to enable it or use a dev-token."
+ "This server uses dev-token auth. Copy the token from the server and run: `fabro auth login --dev-token `"
);
assert_eq!(
login_failure_message("server_error", Some("SESSION_SECRET is not configured")),
diff --git a/lib/crates/fabro-cli/src/commands/auth/logout.rs b/lib/crates/fabro-cli/src/commands/auth/logout.rs
index 4ccdfd270..52009485e 100644
--- a/lib/crates/fabro-cli/src/commands/auth/logout.rs
+++ b/lib/crates/fabro-cli/src/commands/auth/logout.rs
@@ -1,5 +1,5 @@
use anyhow::{Result, bail};
-use fabro_client::{AuthEntry, AuthStore};
+use fabro_client::{AuthEntry, AuthStore, OAuthEntry};
use fabro_http::header::AUTHORIZATION;
use crate::args::AuthLogoutArgs;
@@ -21,8 +21,10 @@ pub(super) async fn logout_command(args: AuthLogoutArgs, base_ctx: &CommandConte
let mut warnings = Vec::new();
for (target, entry) in entries {
- if let Err(error) = revoke_remote_session(&target, &entry).await {
- warnings.push(format_warning(&target, &error.to_string()));
+ if let AuthEntry::OAuth(entry) = &entry {
+ if let Err(error) = revoke_remote_session(&target, entry).await {
+ warnings.push(format_warning(&target, &error.to_string()));
+ }
}
store.remove(&target)?;
}
@@ -40,15 +42,17 @@ pub(super) async fn logout_command(args: AuthLogoutArgs, base_ctx: &CommandConte
return Ok(());
};
- if let Err(error) = revoke_remote_session(&target, &entry).await {
- fabro_util::printerr!(printer, "{}", format_warning(&target, &error.to_string()));
+ if let AuthEntry::OAuth(entry) = &entry {
+ if let Err(error) = revoke_remote_session(&target, entry).await {
+ fabro_util::printerr!(printer, "{}", format_warning(&target, &error.to_string()));
+ }
}
store.remove(&target)?;
fabro_util::printerr!(printer, "Logged out from {}.", target);
Ok(())
}
-async fn revoke_remote_session(target: &ServerTarget, entry: &AuthEntry) -> Result<()> {
+async fn revoke_remote_session(target: &ServerTarget, entry: &OAuthEntry) -> Result<()> {
let (http_client, base_url) = target.build_public_http_client()?;
let response = http_client
.post(format!("{base_url}/auth/cli/logout"))
diff --git a/lib/crates/fabro-cli/src/commands/auth/status.rs b/lib/crates/fabro-cli/src/commands/auth/status.rs
index 692c1f381..f5bf3272f 100644
--- a/lib/crates/fabro-cli/src/commands/auth/status.rs
+++ b/lib/crates/fabro-cli/src/commands/auth/status.rs
@@ -1,8 +1,9 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
-use fabro_client::{AuthEntry, AuthStore};
+use fabro_client::{AuthEntry, AuthStore, OAuthEntry};
use fabro_static::EnvVars;
-use fabro_util::dev_token::{read_dev_token_file, validate_dev_token_format};
+use fabro_util::dev_token::validate_dev_token_format;
+use fabro_util::printer::Printer;
use serde::Serialize;
use crate::args::AuthStatusArgs;
@@ -20,23 +21,32 @@ enum OAuthState {
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-struct StatusRow {
- server: String,
- oauth_state: OAuthState,
- access_token_expires_at: DateTime,
- refresh_token_expires_at: DateTime,
- logged_in_at: DateTime,
- login: String,
- name: String,
- email: String,
- idp_issuer: String,
- idp_subject: String,
+#[serde(tag = "kind")]
+enum StatusRow {
+ #[serde(rename = "oauth")]
+ OAuth {
+ server: String,
+ oauth_state: OAuthState,
+ access_token_expires_at: DateTime,
+ refresh_token_expires_at: DateTime,
+ logged_in_at: DateTime,
+ login: String,
+ name: String,
+ email: String,
+ idp_issuer: String,
+ idp_subject: String,
+ },
+ #[serde(rename = "dev-token")]
+ DevToken {
+ server: String,
+ logged_in_at: DateTime,
+ },
}
#[derive(Serialize)]
struct StatusOutput {
- servers: Vec,
- dev_token: &'static str,
+ servers: Vec,
+ env_dev_token: &'static str,
}
pub(super) fn status_command(args: &AuthStatusArgs, ctx: &CommandContext) -> Result<()> {
@@ -49,7 +59,7 @@ pub(super) fn status_command(args: &AuthStatusArgs, ctx: &CommandContext) -> Res
} else {
all_rows(&store, now)?
};
- let dev_token = if load_dev_token_if_available() {
+ let env_dev_token = if load_env_dev_token_if_available() {
"active"
} else {
"not_set"
@@ -58,14 +68,14 @@ pub(super) fn status_command(args: &AuthStatusArgs, ctx: &CommandContext) -> Res
if ctx.explicit_json_requested() {
print_json_pretty(&StatusOutput {
servers: rows,
- dev_token,
+ env_dev_token,
})?;
return Ok(());
}
if rows.is_empty() {
fabro_util::printerr!(printer, "Not logged in to any servers.");
- fabro_util::printerr!(printer, "Dev token: {dev_token}");
+ print_env_dev_token_status(env_dev_token, printer);
return Ok(());
}
@@ -73,44 +83,65 @@ pub(super) fn status_command(args: &AuthStatusArgs, ctx: &CommandContext) -> Res
if index > 0 {
fabro_util::printerr!(printer, "");
}
- fabro_util::printerr!(printer, "{}", row.server);
- fabro_util::printerr!(
- printer,
- " OAuth: {} as {}",
- human_state(row.oauth_state),
- row.login
- );
- fabro_util::printerr!(
- printer,
- " Name: {}",
- if row.name.is_empty() {
- "(not set)"
- } else {
- row.name.as_str()
+ match row {
+ StatusRow::OAuth {
+ server,
+ oauth_state,
+ access_token_expires_at,
+ refresh_token_expires_at,
+ login,
+ name,
+ email,
+ ..
+ } => {
+ fabro_util::printerr!(printer, "{server}");
+ fabro_util::printerr!(
+ printer,
+ " OAuth: {} as {}",
+ human_state(*oauth_state),
+ login
+ );
+ fabro_util::printerr!(
+ printer,
+ " Name: {}",
+ if name.is_empty() {
+ "(not set)"
+ } else {
+ name.as_str()
+ }
+ );
+ fabro_util::printerr!(
+ printer,
+ " Email: {}",
+ if email.is_empty() {
+ "(not set)"
+ } else {
+ email.as_str()
+ }
+ );
+ fabro_util::printerr!(
+ printer,
+ " Access expires: {}",
+ access_token_expires_at.to_rfc3339()
+ );
+ fabro_util::printerr!(
+ printer,
+ " Refresh expires: {}",
+ refresh_token_expires_at.to_rfc3339()
+ );
}
- );
- fabro_util::printerr!(
- printer,
- " Email: {}",
- if row.email.is_empty() {
- "(not set)"
- } else {
- row.email.as_str()
+ StatusRow::DevToken {
+ server,
+ logged_in_at,
+ } => {
+ fabro_util::printerr!(printer, "{server}");
+ fabro_util::printerr!(printer, " Auth: dev-token");
+ fabro_util::printerr!(printer, " Logged in: {}", logged_in_at.to_rfc3339());
}
- );
- fabro_util::printerr!(
- printer,
- " Access expires: {}",
- row.access_token_expires_at.to_rfc3339()
- );
- fabro_util::printerr!(
- printer,
- " Refresh expires: {}",
- row.refresh_token_expires_at.to_rfc3339()
- );
+ }
}
fabro_util::printerr!(printer, "");
- fabro_util::printerr!(printer, "Dev token: {dev_token}");
+ print_env_dev_token_status(env_dev_token, printer);
Ok(())
}
@@ -135,21 +166,27 @@ fn filter_rows(
}
fn status_row(target: &ServerTarget, entry: AuthEntry, now: DateTime) -> StatusRow {
- StatusRow {
- server: target.to_string(),
- oauth_state: oauth_state(&entry, now),
- access_token_expires_at: entry.access_token_expires_at,
- refresh_token_expires_at: entry.refresh_token_expires_at,
- logged_in_at: entry.logged_in_at,
- login: entry.subject.login,
- name: entry.subject.name,
- email: entry.subject.email,
- idp_issuer: entry.subject.idp_issuer,
- idp_subject: entry.subject.idp_subject,
+ match entry {
+ AuthEntry::OAuth(entry) => StatusRow::OAuth {
+ server: target.to_string(),
+ oauth_state: oauth_state(&entry, now),
+ access_token_expires_at: entry.access_token_expires_at,
+ refresh_token_expires_at: entry.refresh_token_expires_at,
+ logged_in_at: entry.logged_in_at,
+ login: entry.subject.login,
+ name: entry.subject.name,
+ email: entry.subject.email,
+ idp_issuer: entry.subject.idp_issuer,
+ idp_subject: entry.subject.idp_subject,
+ },
+ AuthEntry::DevToken(entry) => StatusRow::DevToken {
+ server: target.to_string(),
+ logged_in_at: entry.logged_in_at,
+ },
}
}
-fn oauth_state(entry: &AuthEntry, now: DateTime) -> OAuthState {
+fn oauth_state(entry: &OAuthEntry, now: DateTime) -> OAuthState {
if entry.access_token_expires_at > now {
OAuthState::Active
} else if entry.refresh_token_expires_at > now {
@@ -159,6 +196,17 @@ fn oauth_state(entry: &AuthEntry, now: DateTime) -> OAuthState {
}
}
+fn print_env_dev_token_status(env_dev_token: &str, printer: Printer) {
+ if env_dev_token == "active" {
+ fabro_util::printerr!(
+ printer,
+ "FABRO_DEV_TOKEN: active (overrides persisted credentials)"
+ );
+ } else {
+ fabro_util::printerr!(printer, "FABRO_DEV_TOKEN: not_set");
+ }
+}
+
fn human_state(state: OAuthState) -> &'static str {
match state {
OAuthState::Active => "active",
@@ -171,24 +219,23 @@ fn human_state(state: OAuthState) -> &'static str {
clippy::disallowed_methods,
reason = "Auth status reports whether the documented dev-token env source is configured."
)]
-fn load_dev_token_if_available() -> bool {
+fn load_env_dev_token_if_available() -> bool {
let env_token = std::env::var(EnvVars::FABRO_DEV_TOKEN)
.ok()
.filter(|token| validate_dev_token_format(token));
env_token.is_some()
- || read_dev_token_file(&fabro_util::Home::from_env().dev_token_path()).is_some()
}
#[cfg(test)]
mod tests {
use chrono::Duration;
- use fabro_client::{AuthEntry, StoredSubject};
+ use fabro_client::{OAuthEntry, StoredSubject};
use super::{OAuthState, human_state, oauth_state};
- fn entry(access_offset_secs: i64, refresh_offset_secs: i64) -> AuthEntry {
+ fn entry(access_offset_secs: i64, refresh_offset_secs: i64) -> OAuthEntry {
let now = chrono::Utc::now();
- AuthEntry {
+ OAuthEntry {
access_token: "access".to_string(),
access_token_expires_at: now + Duration::seconds(access_offset_secs),
refresh_token: "refresh".to_string(),
diff --git a/lib/crates/fabro-cli/src/commands/install.rs b/lib/crates/fabro-cli/src/commands/install.rs
index 5e3ac7915..1930812ba 100644
--- a/lib/crates/fabro-cli/src/commands/install.rs
+++ b/lib/crates/fabro-cli/src/commands/install.rs
@@ -21,6 +21,7 @@ use dialoguer::theme::ColorfulTheme;
use dialoguer::{MultiSelect, Select};
use fabro_api::types::{CreateSecretRequest, SecretType as ApiSecretType};
use fabro_auth::{AuthCredential, AuthMethod, codex_oauth_config, credential_id_for};
+use fabro_client::{AuthEntry, AuthStore, DevTokenEntry, ServerTarget};
use fabro_config::bind::Bind;
use fabro_config::daemon::ServerDaemon;
use fabro_config::user::{SETTINGS_CONFIG_FILENAME, default_storage_dir};
@@ -1773,6 +1774,7 @@ async fn run_install_inner(args: &InstallArgs, ctx: &CommandContext) -> Result<(
let install_server_settings = fabro_config::ServerSettingsBuilder::from_toml(&settings_toml)?;
// Secrets and auth material
+ let mut dev_token_for_auth_store = None;
{
let session_secret = session_secret::generate_session_secret();
fabro_util::printerr!(
@@ -1787,15 +1789,11 @@ async fn run_install_inner(args: &InstallArgs, ctx: &CommandContext) -> Result<(
.methods
.contains(&ServerAuthMethod::DevToken)
{
- let token = dev_token::read_or_mint_dev_token_for_install(
- &fabro_util::Home::from_env().dev_token_path(),
- )?;
- dev_token::write_dev_token(
- &Storage::new(&storage_dir)
- .runtime_directory()
- .dev_token_path(),
- &token,
- )?;
+ let dev_token_path = Storage::new(&storage_dir)
+ .runtime_directory()
+ .dev_token_path();
+ let token = dev_token::read_or_mint_dev_token_for_install(&dev_token_path)?;
+ dev_token_for_auth_store = Some(token.clone());
fabro_util::printerr!(
printer,
" {} Development token generated",
@@ -1825,6 +1823,22 @@ async fn run_install_inner(args: &InstallArgs, ctx: &CommandContext) -> Result<(
server_was_running,
)
.await?;
+ if let Some(token) = dev_token_for_auth_store {
+ let target = ServerTarget::http_url(&args.web_url)?;
+ if let Err(err) = AuthStore::default().put(
+ &target,
+ AuthEntry::DevToken(DevTokenEntry {
+ token,
+ logged_in_at: chrono::Utc::now(),
+ }),
+ ) {
+ fabro_util::printerr!(
+ printer,
+ " {} Installed successfully, but failed to save CLI auth: {err}",
+ s.yellow.apply_to("Warning:")
+ );
+ }
+ }
if let Err(err) = write_artifact_store_metadata(&install_server_settings, FABRO_VERSION).await {
fabro_util::printerr!(
printer,
diff --git a/lib/crates/fabro-cli/src/commands/server/mod.rs b/lib/crates/fabro-cli/src/commands/server/mod.rs
index 9572643c6..687010146 100644
--- a/lib/crates/fabro-cli/src/commands/server/mod.rs
+++ b/lib/crates/fabro-cli/src/commands/server/mod.rs
@@ -3,14 +3,16 @@ pub(crate) mod start;
pub(crate) mod status;
pub(crate) mod stop;
+use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
+use fabro_client::{AuthEntry, AuthStore, DevTokenEntry, ServerTarget};
use fabro_config::bind::{self, Bind, BindRequest};
use fabro_config::user::{active_settings_path, default_storage_dir};
-use fabro_server::install::{self, InstallAppState};
+use fabro_server::install::{self, InstallAppState, InstallFinishHook, InstallFinishInfo};
use fabro_server::serve::{self, ServeArgs};
use fabro_static::EnvVars;
use fabro_util::browser;
@@ -207,7 +209,8 @@ async fn run_install_mode(bootstrap: InstallBootstrap, printer: Printer) -> Resu
bootstrap.token,
&bootstrap.storage_dir,
&bootstrap.config_path,
- );
+ )
+ .with_finish_hook(persist_install_dev_token_hook());
install::serve_install_command(bootstrap.bind_request, state, move |bind| {
announce_install_mode(bind, &token, styles, printer);
Ok(())
@@ -215,6 +218,23 @@ async fn run_install_mode(bootstrap: InstallBootstrap, printer: Printer) -> Resu
.await
}
+fn persist_install_dev_token_hook() -> InstallFinishHook {
+ Arc::new(|info: &InstallFinishInfo| {
+ let Some(token) = &info.dev_token else {
+ return Ok(());
+ };
+ let target = ServerTarget::http_url(&info.canonical_url)?;
+ AuthStore::default().put(
+ &target,
+ AuthEntry::DevToken(DevTokenEntry {
+ token: token.clone(),
+ logged_in_at: chrono::Utc::now(),
+ }),
+ )?;
+ Ok(())
+ })
+}
+
fn announce_install_mode(bind: &Bind, token: &str, styles: &Styles, printer: Printer) {
info!(
bind = %bind,
diff --git a/lib/crates/fabro-cli/src/server_client.rs b/lib/crates/fabro-cli/src/server_client.rs
index b8995681c..eb3dfe725 100644
--- a/lib/crates/fabro-cli/src/server_client.rs
+++ b/lib/crates/fabro-cli/src/server_client.rs
@@ -1,45 +1,32 @@
use std::net::IpAddr;
use std::path::Path;
-use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context as _, Result, anyhow, bail};
use fabro_client::{
- AuthStore, Credential, CredentialFallback, OAuthSession, ServerTarget, TransportConnector,
+ AuthEntry, AuthStore, Credential, OAuthSession, ServerTarget, TransportConnector,
apply_bearer_token_auth,
};
pub(crate) use fabro_client::{Client, RunEventStream};
+use fabro_config::Storage;
use fabro_config::bind::Bind;
use fabro_static::EnvVars;
pub(crate) use fabro_types::RunProjection;
use fabro_types::UserSettings;
+use fabro_util::dev_token;
use fabro_util::dev_token::validate_dev_token_format;
-use fabro_util::{Home, dev_token};
use tokio::time::sleep;
use crate::args::ServerTargetArgs;
use crate::commands::server::start;
use crate::user_config::{self, cli_http_client_builder};
-#[derive(Debug)]
-struct CliDevTokenFallback;
-
-impl CredentialFallback for CliDevTokenFallback {
- fn resolve(&self) -> Option {
- load_cli_dev_token().map(Credential::DevToken)
- }
-}
-
fn refreshable_oauth(
target: &ServerTarget,
credential: Option<&Credential>,
) -> Option {
if matches!(credential, Some(Credential::OAuth(_))) {
- let session = OAuthSession::new(target.clone(), AuthStore::default());
- if local_dev_token_fallback(target) {
- return Some(session.with_fallback(Arc::new(CliDevTokenFallback)));
- }
- return Some(session);
+ return Some(OAuthSession::new(target.clone(), AuthStore::default()));
}
None
}
@@ -92,22 +79,34 @@ async fn connect_managed_unix_socket_api_client_bundle(
active_config_path: &Path,
) -> Result {
let target = ServerTarget::unix_socket_path(path)?;
- let credential = resolve_target_credential(&target, local_dev_token_fallback(&target))?;
- let oauth_session = refreshable_oauth(&target, credential.as_ref());
- let bearer_token = credential.as_ref().map(Credential::bearer_token);
+ let runtime_token_path = Storage::new(storage_dir)
+ .runtime_directory()
+ .dev_token_path();
+ let pre_spawn_credential = resolve_target_credential(&target)?
+ .or_else(|| dev_token::read_dev_token_file(&runtime_token_path).map(Credential::DevToken));
+ let pre_spawn_bearer = pre_spawn_credential.as_ref().map(Credential::bearer_token);
- let http_client = if let Ok(http_client) =
- try_connect_unix_socket_http_client(path, true, bearer_token).await
+ let (http_client, credential) = if let Ok(http_client) =
+ try_connect_unix_socket_http_client(path, pre_spawn_bearer).await
{
- http_client
+ (http_client, pre_spawn_credential)
} else {
start::ensure_server_running_on_socket(path, active_config_path, storage_dir)
.await
.with_context(|| format!("Failed to start fabro server for {}", path.display()))?;
- connect_unix_socket_http_client(path, true, bearer_token)
+ let post_spawn_credential = match resolve_target_credential(&target)? {
+ Some(credential) => Some(credential),
+ None => Some(Credential::DevToken(
+ wait_for_runtime_dev_token(&runtime_token_path).await?,
+ )),
+ };
+ let post_spawn_bearer = post_spawn_credential.as_ref().map(Credential::bearer_token);
+ let http_client = connect_unix_socket_http_client(path, post_spawn_bearer)
.await
- .with_context(|| format!("Failed to connect to fabro server at {}", path.display()))?
+ .with_context(|| format!("Failed to connect to fabro server at {}", path.display()))?;
+ (http_client, post_spawn_credential)
};
+ let oauth_session = refreshable_oauth(&target, credential.as_ref());
build_client(
target,
@@ -127,12 +126,16 @@ async fn connect_local_api_client_bundle(
.with_context(|| format!("Failed to start fabro server for {}", storage_dir.display()))?;
match bind {
Bind::Unix(path) => {
- let http_client = connect_unix_socket_http_client(&path, true, None).await?;
+ let runtime_token_path = Storage::new(storage_dir)
+ .runtime_directory()
+ .dev_token_path();
+ let token = wait_for_runtime_dev_token(&runtime_token_path).await?;
+ let http_client = connect_unix_socket_http_client(&path, Some(&token)).await?;
Ok(Client::from_http_client("http://fabro", http_client))
}
Bind::Tcp(addr) => {
let target = ServerTarget::http_url(format!("http://{addr}"))?;
- let credential = resolve_local_tcp_credential(&target)?;
+ let credential = resolve_target_credential(&target)?;
let oauth_session = refreshable_oauth(&target, credential.as_ref());
build_client(target, credential, oauth_session, None).await
}
@@ -140,7 +143,7 @@ async fn connect_local_api_client_bundle(
}
async fn connect_target_api_client_bundle(target: &ServerTarget) -> Result {
- let credential = resolve_target_credential(target, local_dev_token_fallback(target))?;
+ let credential = resolve_target_credential(target)?;
let oauth_session = refreshable_oauth(target, credential.as_ref());
build_client(target.clone(), credential, oauth_session, None).await
}
@@ -204,15 +207,6 @@ fn connect_cli_target_transport(
Ok((http_client, "http://fabro".to_string()))
}
-fn local_dev_token_fallback(target: &ServerTarget) -> bool {
- target.is_unix_socket()
-}
-
-fn load_cli_dev_token() -> Option {
- let env_token = process_env_var(EnvVars::FABRO_DEV_TOKEN);
- load_cli_dev_token_from_sources(env_token.as_deref(), &Home::from_env())
-}
-
#[expect(
clippy::disallowed_methods,
reason = "Server client authentication supports the documented local dev-token env source."
@@ -221,38 +215,29 @@ fn process_env_var(name: &str) -> Option {
std::env::var(name).ok()
}
-fn load_cli_dev_token_from_sources(env_token: Option<&str>, home: &Home) -> Option {
- if let Some(token) = env_token.filter(|token| validate_dev_token_format(token)) {
- return Some(token.to_owned());
- }
-
- dev_token::read_dev_token_file(&home.dev_token_path())
-}
-
-async fn wait_for_cli_dev_token() -> Result {
+async fn wait_for_runtime_dev_token(path: &Path) -> Result {
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while std::time::Instant::now() < deadline {
- if let Some(token) = load_cli_dev_token() {
+ if let Some(token) = dev_token::read_dev_token_file(path) {
return Ok(token);
}
sleep(Duration::from_millis(50)).await;
}
- bail!("local CLI dev token did not become available");
+ bail!(
+ "runtime dev token did not become available at {}",
+ path.display()
+ );
}
-async fn build_authed_unix_socket_http_client(
+fn build_authed_unix_socket_http_client(
path: &Path,
- wait_for_cli_dev_token_fallback: bool,
bearer_token: Option<&str>,
) -> Result {
let builder = cli_http_client_builder().unix_socket(path).no_proxy();
let builder = if let Some(token) = bearer_token {
apply_bearer_token_auth(builder, token)?
- } else if wait_for_cli_dev_token_fallback {
- let token = wait_for_cli_dev_token().await?;
- apply_bearer_token_auth(builder, &token)?
} else {
builder
};
@@ -272,37 +257,21 @@ fn build_unix_socket_probe_client(path: &Path) -> Result
async fn try_connect_unix_socket_http_client(
path: &Path,
- wait_for_cli_dev_token_fallback: bool,
bearer_token: Option<&str>,
) -> Result {
check_server_ready(&build_unix_socket_probe_client(path)?).await?;
- build_authed_unix_socket_http_client(path, wait_for_cli_dev_token_fallback, bearer_token).await
+ build_authed_unix_socket_http_client(path, bearer_token)
}
async fn connect_unix_socket_http_client(
path: &Path,
- wait_for_cli_dev_token_fallback: bool,
bearer_token: Option<&str>,
) -> Result {
wait_for_server_ready(&build_unix_socket_probe_client(path)?).await?;
- build_authed_unix_socket_http_client(path, wait_for_cli_dev_token_fallback, bearer_token).await
+ build_authed_unix_socket_http_client(path, bearer_token)
}
-fn resolve_oauth_credential(
- target: &ServerTarget,
- store: &AuthStore,
- now: chrono::DateTime,
-) -> Result