use std::process::ExitCode;
use link_assistant_router::cli::{AuthOp, ImportProvider};
use link_assistant_router::subscription::{
ImportSource, InstallDocumentResult, InstallMode, SubscriptionProvider, SubscriptionReader,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
enum CredentialProbe {
Accepted,
Rejected,
Unverified,
}
#[derive(Debug, Clone, Copy, Default)]
struct ImportPolicy {
if_absent: bool,
capability_asserted: bool,
}
struct ValidatedCandidate {
document: String,
token: link_assistant_router::subscription::SubscriptionToken,
stage: tempfile::TempDir,
transaction_id: String,
}
impl std::fmt::Debug for ValidatedCandidate {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ValidatedCandidate")
.field("provider", &"redacted")
.field("transaction_id", &self.transaction_id)
.finish_non_exhaustive()
}
}
impl ValidatedCandidate {
fn retain(self) -> String {
let Self {
stage,
transaction_id,
..
} = self;
let _retained_path = stage.keep();
transaction_id
}
}
pub async fn run_import(
config: &link_assistant_router::config::Config,
op: &AuthOp,
) -> Option<ExitCode> {
if let Some(exit) = refuse_a_remote_import(op).await {
return Some(exit);
}
let policy = match op {
AuthOp::Import {
if_absent, force, ..
} => ImportPolicy {
if_absent: *if_absent,
capability_asserted: *force,
},
_ => ImportPolicy::default(),
};
let requested: Vec<(ImportProvider, String)> = match op {
AuthOp::Claude {
from_claude_home: Some(source),
..
} => vec![(ImportProvider::Claude, source.clone())],
AuthOp::Codex {
from_codex_home: Some(source),
..
} => vec![(ImportProvider::Codex, source.clone())],
AuthOp::Import { all: true, .. } => [
ImportProvider::Claude,
ImportProvider::Codex,
ImportProvider::Gemini,
ImportProvider::Qwen,
ImportProvider::Gh,
]
.into_iter()
.map(|provider| (provider, String::new()))
.collect(),
AuthOp::Import {
provider: Some(provider),
dir,
..
} => vec![(*provider, dir.clone().unwrap_or_default())],
_ => return None,
};
let adopting_everything = matches!(op, AuthOp::Import { all: true, .. });
let mut failed = false;
for (provider, source) in requested {
let outcome = match provider {
ImportProvider::Gh if policy.if_absent => Err(String::from(
"--if-absent is supported only for Claude, Codex, Gemini, and Qwen; GitHub import keeps its existing replacement behavior",
)),
ImportProvider::Gh => import_github(&config.data_dir, &source),
other => {
let Some(subscription) = subscription_of(other) else {
continue;
};
import_provider(config, subscription, &source, policy).await
}
};
if let Err(error) = outcome {
if adopting_everything && error.starts_with("no ") {
println!("{}: nothing to adopt ({error})", provider_label(provider));
continue;
}
eprintln!("error: {error}");
failed = true;
}
}
Some(if failed {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
})
}
async fn refuse_a_remote_import(op: &AuthOp) -> Option<ExitCode> {
if !op.may_be_remote() {
return None;
}
let AuthOp::Import { target, .. } = op else {
return None;
};
let server = match link_assistant_router::auth_remote::target_for(
target.local,
target.managed,
target.server.as_deref(),
target.management_server.as_deref(),
)
.await
{
Ok(Some(server)) => server,
Ok(None) => return None,
Err(error) => {
eprintln!("error: {error}");
return Some(ExitCode::from(1));
}
};
let destination = match op {
AuthOp::Import {
provider: Some(provider),
..
} => {
link_assistant_router::auth_remote::credential_home(&server, provider_label(*provider))
.await
}
_ => None,
};
for line in link_assistant_router::auth_remote::remote_import_refusal(
&server.base_url,
destination.as_deref(),
) {
eprintln!("{line}");
}
Some(ExitCode::from(1))
}
const fn subscription_of(provider: ImportProvider) -> Option<SubscriptionProvider> {
match provider {
ImportProvider::Claude => Some(SubscriptionProvider::Claude),
ImportProvider::Codex => Some(SubscriptionProvider::Codex),
ImportProvider::Gemini => Some(SubscriptionProvider::Gemini),
ImportProvider::Qwen => Some(SubscriptionProvider::Qwen),
ImportProvider::Gh => None,
}
}
const fn provider_label(provider: ImportProvider) -> &'static str {
match provider {
ImportProvider::Claude => "claude",
ImportProvider::Codex => "codex",
ImportProvider::Gemini => "gemini",
ImportProvider::Qwen => "qwen",
ImportProvider::Gh => "github",
}
}
fn import_github(data_dir: &std::path::Path, source: &str) -> Result<(), String> {
use link_assistant_router::github_proxy;
let directory = Some(source)
.filter(|source| !source.trim().is_empty())
.map(std::path::PathBuf::from)
.or_else(github_proxy::gh_config_directory)
.ok_or_else(|| {
String::from("no gh configuration directory; name one, or set GH_CONFIG_DIR")
})?;
let token = github_proxy::token_from_gh_config(&directory).ok_or_else(|| {
format!(
"no GitHub credential in {}; run `gh auth login` there first",
directory.display()
)
})?;
let path = github_proxy::store_credential(data_dir, &token)?;
println!(
"github imported {} from {}",
path.display(),
directory.display()
);
println!("github note: the GitHub routes are mounted at startup; restart to serve them");
Ok(())
}
async fn import_provider(
config: &link_assistant_router::config::Config,
provider: SubscriptionProvider,
source: &str,
policy: ImportPolicy,
) -> Result<(), String> {
let user_home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
let source_home = if source.trim().is_empty() {
provider.conventional_home(&user_home)
} else {
std::path::PathBuf::from(source)
};
let destination_home = provider_home(config, provider, &user_home);
if same_credential_home(&source_home, &destination_home) {
return Err(format!(
"{provider} is already read from {}, so there is nothing to adopt",
destination_home.display()
));
}
let from = SubscriptionReader::new(provider, &source_home);
let source_credential = from
.read_document_for_import()
.map_err(|error| match error {
link_assistant_router::subscription::SubscriptionError::NoCredentials(message) => {
format!("no {provider} credential to import: {message}")
}
other => format!("invalid {provider} candidate credential: {other}"),
})?;
let ImportSource {
document,
token: _,
origin,
} = &source_credential;
let where_from = match origin {
link_assistant_router::platform_keychain::Origin::Keychain => {
link_assistant_router::platform_keychain::service_name(provider).map_or_else(
|| String::from("the platform keychain"),
|service| format!("keychain {service:?}"),
)
}
link_assistant_router::platform_keychain::Origin::File => {
from.discover_credential_path().map_or_else(
|| source_home.display().to_string(),
|path| path.display().to_string(),
)
}
};
let destination = SubscriptionReader::new(provider, &destination_home);
if policy.if_absent && destination.has_platform_store_credential() {
println!(
"{provider:<8} already present in the platform credential store; candidate from {where_from} was not validated or installed"
);
return Ok(());
}
if policy.if_absent
&& let Some(path) = destination
.existing_document_locked(
&config.data_dir,
link_assistant_router::credential_recovery_store::PRIMARY_ACCOUNT,
)
.await?
{
println!(
"{provider:<8} already present at {}; candidate from {where_from} was not validated or installed",
path.display()
);
return Ok(());
}
let validated = validate_candidate(&config.data_dir, provider, document).await?;
let report = describe_credential(&validated.token);
if (policy.if_absent && destination.has_platform_store_credential())
|| (!policy.if_absent
&& destination.candidate_is_shadowed_by_platform_store(&validated.token))
{
let transaction_id = validated.retain();
return Err(format!(
"the {provider} platform credential remains authoritative; validated candidate retained as transaction {transaction_id}"
));
}
let promotion = install_candidate(
&destination,
&config.data_dir,
&validated.document,
CredentialProbe::Accepted,
policy,
)
.await;
let installed = match promotion {
Ok(installed) => installed,
Err(error) => {
let transaction_id = validated.retain();
return Err(format!(
"{error}; validated candidate retained as transaction {transaction_id}"
));
}
};
match installed {
InstallDocumentResult::Installed(path) => {
println!(
"{provider:<8} imported {} from {where_from}",
path.display()
);
println!(
"{provider:<8} note: refresh-chain validation advanced the candidate before \
installation; the source copy may now contain the spent predecessor"
);
drop(validated);
}
InstallDocumentResult::AlreadyPresent(path) => {
let transaction_id = validated.retain();
println!(
"{provider:<8} already present at {}; candidate from {where_from} was not installed",
path.display()
);
println!("{provider:<8} validated candidate retained as transaction {transaction_id}");
}
}
println!(
"{provider:<8} candidate {report}, accepted by the vendor after refresh-chain validation"
);
Ok(())
}
async fn validate_candidate(
data_dir: &std::path::Path,
provider: SubscriptionProvider,
document: &str,
) -> Result<ValidatedCandidate, String> {
import_refresh_prerequisite(provider, |key| std::env::var(key).ok())?;
validate_candidate_with(data_dir, provider, document, None, None).await
}
fn import_refresh_prerequisite(
provider: SubscriptionProvider,
lookup: impl FnOnce(&str) -> Option<String>,
) -> Result<(), String> {
if provider != SubscriptionProvider::Gemini {
return Ok(());
}
let variable = link_assistant_router::refresh::GEMINI_CLIENT_SECRET_ENV;
if lookup(variable).is_some_and(|value| !value.trim().is_empty()) {
return Ok(());
}
Err(format!(
"Gemini refresh-chain import requires {variable}; set it to the OAuth client secret shipped with Gemini CLI"
))
}
fn same_credential_home(source: &std::path::Path, destination: &std::path::Path) -> bool {
if source == destination {
return true;
}
match (
std::fs::canonicalize(source),
std::fs::canonicalize(destination),
) {
(Ok(source), Ok(destination)) => source == destination,
_ => false,
}
}
async fn validate_candidate_with(
data_dir: &std::path::Path,
provider: SubscriptionProvider,
document: &str,
token_url_override: Option<&str>,
catalog_base_url_override: Option<&str>,
) -> Result<ValidatedCandidate, String> {
let staging_root = data_dir.join("auth-import-candidates");
std::fs::create_dir_all(&staging_root)
.map_err(|_| "could not create the private credential-import staging area".to_string())?;
let transaction_id = uuid::Uuid::new_v4().simple().to_string();
let stage = tempfile::Builder::new()
.prefix(&format!("{transaction_id}-"))
.tempdir_in(&staging_root)
.map_err(|_| "could not create a private credential-import transaction".to_string())?;
let candidate_home = stage.path().join(provider.as_str());
std::fs::create_dir(&candidate_home)
.map_err(|_| "could not create the isolated candidate store".to_string())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(&candidate_home, std::fs::Permissions::from_mode(0o700))
.map_err(|_| "could not protect the isolated candidate store".to_string())?;
}
let reader = SubscriptionReader::new(provider, &candidate_home);
reader
.install_document(document)
.map_err(|_| format!("the {provider} candidate could not be staged durably"))?;
let catalog_base = if let Some(base) = catalog_base_url_override {
base.trim_end_matches('/').to_string()
} else {
let staged = reader.read_document_for_import().map_err(|_| {
format!("the staged {provider} candidate could not be read for validation")
})?;
catalog_base_for_candidate(provider, &staged.token)?
};
let candidate_data = stage.path().join("router-state");
let cache = link_assistant_router::refresh::TokenCache::registered_for(
std::slice::from_ref(&reader),
&candidate_data,
);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(20))
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(|_| "could not initialize candidate validation".to_string())?;
let now_ms = chrono::Utc::now().timestamp_millis();
let refresh_result = match token_url_override {
Some(token_url) => {
cache
.validate_refresh_chain_registered_at(
&client,
token_url,
provider,
link_assistant_router::credential_recovery_store::PRIMARY_ACCOUNT,
now_ms,
)
.await
}
None => {
cache
.validate_refresh_chain_registered(
&client,
provider,
link_assistant_router::credential_recovery_store::PRIMARY_ACCOUNT,
now_ms,
)
.await
}
};
let refreshed = match refresh_result {
Ok(refreshed) => refreshed,
Err(error) => {
let _retained_path = stage.keep();
return Err(format!(
"{error}; isolated candidate retained as transaction {transaction_id}"
));
}
};
if let Err(error) = link_assistant_router::model_catalog::fetch_provider_catalog(
&client,
provider,
&refreshed,
Some(&catalog_base),
)
.await
{
let reason = if link_assistant_router::model_catalog::is_credential_rejection(&error) {
"was rejected by the vendor catalog"
} else {
"was not positively accepted by the vendor catalog"
};
let _retained_path = stage.keep();
return Err(format!(
"the {provider} candidate {reason}; refreshed candidate retained as transaction {transaction_id}"
));
}
let Ok(durable) = reader.read_document_for_import() else {
let _retained_path = stage.keep();
return Err(format!(
"the durable {provider} candidate could not be reread; refreshed candidate retained as transaction {transaction_id}"
));
};
if durable.token.access_token != refreshed.access_token
|| durable.token.refresh_token != refreshed.refresh_token
{
let _retained_path = stage.keep();
return Err(format!(
"the durable {provider} candidate changed after validation; refreshed candidate retained as transaction {transaction_id}"
));
}
Ok(ValidatedCandidate {
document: durable.document,
token: durable.token,
stage,
transaction_id,
})
}
fn catalog_base_for_candidate(
provider: SubscriptionProvider,
token: &link_assistant_router::subscription::SubscriptionToken,
) -> Result<String, String> {
if provider == SubscriptionProvider::Gemini {
return Ok("https://generativelanguage.googleapis.com".to_string());
}
if provider != SubscriptionProvider::Qwen {
return Ok(provider.default_base_url().to_string());
}
let base = token.base_url(provider);
let parsed = reqwest::Url::parse(&base)
.map_err(|_| "the Qwen candidate names an invalid catalog origin".to_string())?;
let trusted_host = matches!(
parsed.host_str(),
Some("portal.qwen.ai" | "dashscope.aliyuncs.com")
);
let safe_authority = parsed.scheme() == "https"
&& parsed.username().is_empty()
&& parsed.password().is_none()
&& parsed.port_or_known_default() == Some(443)
&& parsed.query().is_none()
&& parsed.fragment().is_none();
if !trusted_host || !safe_authority {
return Err("the Qwen candidate catalog origin is not trusted".to_string());
}
Ok(base.trim_end_matches('/').to_string())
}
async fn install_candidate(
destination: &SubscriptionReader,
data_dir: &std::path::Path,
document: &str,
probe: CredentialProbe,
policy: ImportPolicy,
) -> Result<InstallDocumentResult, String> {
if policy.capability_asserted {
tracing::debug!("caller asserted safe-refresh-chain-import-v1");
}
let refusal = (probe != CredentialProbe::Accepted).then(|| {
format!(
"{} candidate was not accepted by the vendor and cannot be installed",
destination.provider()
)
});
if !policy.if_absent
&& let Some(error) = refusal
{
return Err(error);
}
let mode = if policy.if_absent {
InstallMode::IfAbsent
} else {
InstallMode::Replace
};
destination
.install_document_locked_with_refusal(
data_dir,
link_assistant_router::credential_recovery_store::PRIMARY_ACCOUNT,
document,
mode,
refusal,
)
.await
}
pub fn describe_credential(
token: &link_assistant_router::subscription::SubscriptionToken,
) -> String {
let now = chrono::Utc::now().timestamp_millis();
let expiry = token.expires_at_ms.map_or_else(
|| String::from("no recorded expiry"),
|expires_at| {
let minutes = (expires_at - now) / 60_000;
if expires_at <= now {
format!("EXPIRED {} ago", humanize_minutes(-minutes))
} else {
format!("expires in {}", humanize_minutes(minutes))
}
},
);
let refresh = if token.refresh_token.is_some() {
"refresh token present"
} else {
"NO refresh token, so it cannot be renewed"
};
format!("{expiry}, {refresh}")
}
pub fn humanize_minutes(minutes: i64) -> String {
if minutes < 90 {
return format!("{minutes} minutes");
}
let hours = minutes / 60;
if hours < 48 {
return format!("{hours} hours");
}
format!("{} days", hours / 24)
}
pub fn provider_home(
config: &link_assistant_router::config::Config,
provider: SubscriptionProvider,
user_home: &str,
) -> std::path::PathBuf {
match provider {
SubscriptionProvider::Claude => config.login.claude_code_home.clone(),
SubscriptionProvider::Codex => config.login.codex_home.clone(),
SubscriptionProvider::Gemini | SubscriptionProvider::Qwen => {
provider.resolve_home(user_home)
}
}
}
#[cfg(test)]
#[path = "auth_import_tests.rs"]
mod tests;