use std::io::{self, Write};
use chrono::{DateTime, Duration, Utc};
use costroid_connect::{
AnthropicAdapter, ApiVendor, ConnectError, ConnectionRegistry, CostReportOutcome,
CredentialStore, DateRange, ExposeSecret, OpenAiAdapter, OrgLabel, OrgValidation, SecretString,
VendorReportUnavailable,
};
use costroid_core::vendor_report::AccessForbiddenHint;
#[derive(Clone, Copy)]
pub struct OutputStyle {
pub ascii: bool,
}
pub trait AdapterSet {
fn validate_anthropic(&self, key: &SecretString) -> Result<OrgValidation, ConnectError>;
fn probe_openai(
&self,
key: &SecretString,
range: DateRange,
) -> Result<CostReportOutcome, ConnectError>;
fn cost_report(
&self,
vendor: ApiVendor,
store: &CredentialStore,
range: DateRange,
) -> Result<CostReportOutcome, ConnectError>;
}
pub struct RealAdapters;
impl AdapterSet for RealAdapters {
fn validate_anthropic(&self, key: &SecretString) -> Result<OrgValidation, ConnectError> {
AnthropicAdapter::new()?.validate(key)
}
fn probe_openai(
&self,
key: &SecretString,
range: DateRange,
) -> Result<CostReportOutcome, ConnectError> {
OpenAiAdapter::new()?.fetch_cost_report(key, range)
}
fn cost_report(
&self,
vendor: ApiVendor,
store: &CredentialStore,
range: DateRange,
) -> Result<CostReportOutcome, ConnectError> {
match vendor {
ApiVendor::Anthropic => AnthropicAdapter::new()?.cost_report(store, range),
ApiVendor::OpenAI => OpenAiAdapter::new()?.cost_report(store, range),
ApiVendor::Gemini => Ok(CostReportOutcome::Unavailable(
VendorReportUnavailable::NoSanctionedStaticKeyApi,
)),
}
}
}
enum ValidationResult {
Valid { label: Option<OrgLabel> },
Unavailable(VendorReportUnavailable),
}
pub fn run_connect(
vendor: ApiVendor,
key: SecretString,
adapters: &dyn AdapterSet,
store: &CredentialStore,
registry: &ConnectionRegistry,
out: &mut dyn Write,
style: OutputStyle,
) -> anyhow::Result<i32> {
match validate_vendor(vendor, &key, adapters)? {
ValidationResult::Valid { label } => {
store.store(vendor, &key)?;
registry.mark_connected_with_label(vendor, label.clone())?;
emit(out, style, &connected_message(vendor, label.as_ref()))?;
Ok(0)
}
ValidationResult::Unavailable(reason) => {
emit(
out,
style,
&format!(
"Could not connect {vendor}: {} Nothing was stored.",
remediation(vendor, &reason, &key)
),
)?;
Ok(1)
}
}
}
pub fn gemini_connect(out: &mut dyn Write, style: OutputStyle) -> anyhow::Result<i32> {
emit(
out,
style,
&format!("gemini: {}", gemini_unavailable_message()),
)?;
emit(
out,
style,
"Google publishes no sanctioned static-key usage/billing API, so a key cannot be \
connected. (No key was prompted for, read, or stored.)",
)?;
Ok(0)
}
pub fn run_disconnect(
vendor: ApiVendor,
store: &CredentialStore,
registry: &ConnectionRegistry,
out: &mut dyn Write,
style: OutputStyle,
) -> anyhow::Result<i32> {
store.delete(vendor)?;
registry.mark_disconnected(vendor)?;
emit(out, style, &format!("Disconnected {vendor}."))?;
Ok(0)
}
pub fn run_connections(
check: bool,
adapters: &dyn AdapterSet,
store: &CredentialStore,
registry: &ConnectionRegistry,
out: &mut dyn Write,
style: OutputStyle,
) -> anyhow::Result<i32> {
emit(
out,
style,
"Connections (local; pass --check to verify each over the network):",
)?;
for vendor in ApiVendor::ALL {
let line = match vendor {
ApiVendor::Gemini => {
format!(" {vendor:<10} {}", gemini_unavailable_message())
}
ApiVendor::Anthropic | ApiVendor::OpenAI => {
let connected = registry.is_connected(vendor)? && store.retrieve(vendor)?.is_some();
if !connected {
format!(" {vendor:<10} not connected")
} else {
let mut line = format!(
" {vendor:<10} connected{}",
label_suffix(registry, vendor)?
);
if check {
if let Some(key) = store.retrieve(vendor)? {
match validate_vendor(vendor, &key, adapters)? {
ValidationResult::Valid { .. } => {
line.push_str("; verified just now")
}
ValidationResult::Unavailable(reason) => {
line.push_str(&format!("; check failed — {}", reason.message()))
}
}
}
}
line
}
}
};
emit(out, style, &line)?;
}
emit(
out,
style,
"Disconnect any vendor instantly with: costroid disconnect <vendor>",
)?;
Ok(0)
}
fn validate_vendor(
vendor: ApiVendor,
key: &SecretString,
adapters: &dyn AdapterSet,
) -> Result<ValidationResult, ConnectError> {
match vendor {
ApiVendor::Anthropic => Ok(match adapters.validate_anthropic(key)? {
OrgValidation::Valid(label) => ValidationResult::Valid { label: Some(label) },
OrgValidation::Unavailable(reason) => ValidationResult::Unavailable(reason),
}),
ApiVendor::OpenAI => {
let outcome = adapters.probe_openai(key, completed_day_window())?;
Ok(match outcome {
CostReportOutcome::Available(_) => ValidationResult::Valid { label: None },
CostReportOutcome::Unavailable(reason) => ValidationResult::Unavailable(reason),
})
}
ApiVendor::Gemini => Ok(ValidationResult::Unavailable(
VendorReportUnavailable::NoSanctionedStaticKeyApi,
)),
}
}
fn completed_day_window() -> DateRange {
const DAY: i64 = 86_400;
let now = Utc::now();
let today_midnight = now.timestamp() - now.timestamp().rem_euclid(DAY);
match (
DateTime::<Utc>::from_timestamp(today_midnight - DAY, 0),
DateTime::<Utc>::from_timestamp(today_midnight, 0),
) {
(Some(start), Some(end)) => DateRange::new(start, end),
_ => DateRange::new(now - Duration::days(1), now),
}
}
fn connected_message(vendor: ApiVendor, label: Option<&OrgLabel>) -> String {
let tail = "Key stored in your OS keychain.";
match label {
Some(OrgLabel { name, id: Some(id) }) => {
format!("Connected {vendor} — organization {name} ({id}). {tail}")
}
Some(OrgLabel { name, id: None }) => {
format!("Connected {vendor} — organization {name}. {tail}")
}
None => format!("Connected {vendor}. {tail}"),
}
}
fn label_suffix(registry: &ConnectionRegistry, vendor: ApiVendor) -> Result<String, ConnectError> {
Ok(match registry.label(vendor)? {
Some(OrgLabel { name, id: Some(id) }) => format!(" — organization {name} ({id})"),
Some(OrgLabel { name, id: None }) => format!(" — organization {name}"),
None => String::new(),
})
}
fn remediation(vendor: ApiVendor, reason: &VendorReportUnavailable, key: &SecretString) -> String {
match reason {
VendorReportUnavailable::WrongKeyClass { expected_prefix } => format!(
"that looks like a \"{}\" key; {vendor} usage needs a {expected_prefix}… admin key.",
redacted_prefix(key)
),
VendorReportUnavailable::AuthenticationFailed => {
"the key was rejected (check it is a current admin key).".to_string()
}
VendorReportUnavailable::AccessForbidden { hint } => match hint {
AccessForbiddenHint::IndividualAccount => {
"the Admin API is unavailable for individual accounts — create an organization first."
.to_string()
}
AccessForbiddenHint::MemberNotOwner => {
"use an admin key created by an organization Owner.".to_string()
}
AccessForbiddenHint::AwsOrg => {
"the Admin API is unavailable for Claude-on-AWS organizations.".to_string()
}
AccessForbiddenHint::Unknown => "access was forbidden for this key.".to_string(),
},
other => format!("{}.", other.message()),
}
}
fn redacted_prefix(key: &SecretString) -> String {
const MAX: usize = 8;
let exposed = key.expose_secret();
let mut prefix: String = exposed.chars().take(MAX).collect();
if exposed.chars().nth(MAX).is_some() {
prefix.push_str("...");
}
prefix
}
fn gemini_unavailable_message() -> String {
VendorReportUnavailable::NoSanctionedStaticKeyApi.message()
}
fn emit(out: &mut dyn Write, style: OutputStyle, line: &str) -> io::Result<()> {
let sanitized: String = line.chars().filter(|ch| !ch.is_control()).collect();
let folded: String = if style.ascii {
sanitized
.replace('—', "-")
.replace('…', "...")
.chars()
.map(|ch| if ch.is_ascii() { ch } else { '?' })
.collect()
} else {
sanitized
};
writeln!(out, "{folded}")
}
pub fn print_connect_warning(
out: &mut dyn Write,
style: OutputStyle,
vendor: ApiVendor,
) -> io::Result<()> {
emit(
out,
style,
&format!(
"Heads up: a {vendor} admin key is organization-wide — it can read your whole \
organization's usage and billing (and, depending on the key, manage members and \
keys). Treat it like a root credential."
),
)?;
emit(
out,
style,
&format!(
"Best practice: create a dedicated admin key you can revoke instantly, and revoke it \
in the {vendor} console if this machine is ever compromised. Costroid stores it only \
in your OS keychain — never on disk, in a config file, or in a log — and sends it \
only to {vendor}."
),
)
}
#[cfg(all(test, feature = "connect-test-support"))]
mod tests {
use super::*;
use costroid_connect::test_support::{
install_mock_keychain, ok_json, reply, serve_sequence, MockServer,
};
use std::ffi::OsString;
use std::fs;
use std::path::PathBuf;
#[track_caller]
fn okv<T, E: std::fmt::Debug>(result: Result<T, E>) -> T {
match result {
Ok(value) => value,
Err(err) => panic!("expected Ok, got Err: {err:?}"),
}
}
#[track_caller]
fn somev<T>(value: Option<T>) -> T {
match value {
Some(value) => value,
None => panic!("expected Some, got None"),
}
}
fn style() -> OutputStyle {
OutputStyle { ascii: true }
}
struct TempDir {
path: PathBuf,
}
impl TempDir {
fn new(tag: &str) -> Self {
static COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let path = std::env::temp_dir()
.join(format!("costroid-t10a-{tag}-{}-{n}", std::process::id()));
let _ = fs::remove_dir_all(&path);
okv(fs::create_dir_all(&path));
Self { path }
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
struct LoopbackAdapters {
server: MockServer,
}
impl AdapterSet for LoopbackAdapters {
fn validate_anthropic(&self, key: &SecretString) -> Result<OrgValidation, ConnectError> {
self.server.anthropic_adapter().validate(key)
}
fn probe_openai(
&self,
key: &SecretString,
range: DateRange,
) -> Result<CostReportOutcome, ConnectError> {
self.server.openai_adapter().fetch_cost_report(key, range)
}
fn cost_report(
&self,
vendor: ApiVendor,
store: &CredentialStore,
range: DateRange,
) -> Result<CostReportOutcome, ConnectError> {
match vendor {
ApiVendor::Anthropic => self.server.anthropic_adapter().cost_report(store, range),
ApiVendor::OpenAI => self.server.openai_adapter().cost_report(store, range),
ApiVendor::Gemini => Ok(CostReportOutcome::Unavailable(
VendorReportUnavailable::NoSanctionedStaticKeyApi,
)),
}
}
}
const ME_BODY: &str = r#"{"id":"org-abc","name":"Erens Org","type":"organization"}"#;
const EMPTY_COSTS: &str = r#"{"object":"page","has_more":false,"next_page":null,"data":[]}"#;
#[test]
fn connect_anthropic_stores_secret_only_in_keychain_and_records_the_label() {
install_mock_keychain();
let store = okv(CredentialStore::new());
let dir = TempDir::new("connect-anthropic");
let reg_path = dir.path.join("connections.json");
let registry = ConnectionRegistry::at(reg_path.clone());
let adapters = LoopbackAdapters {
server: serve_sequence(vec![ok_json(ME_BODY)]),
};
let key = SecretString::from("sk-ant-admin-FAKE0001".to_string());
let mut out = Vec::new();
let code = okv(run_connect(
ApiVendor::Anthropic,
key,
&adapters,
&store,
®istry,
&mut out,
style(),
));
assert_eq!(code, 0);
assert!(somev(okv(store.retrieve(ApiVendor::Anthropic)))
.expose_secret()
.starts_with("sk-ant-admin"));
assert!(okv(registry.is_connected(ApiVendor::Anthropic)));
assert_eq!(
somev(okv(registry.label(ApiVendor::Anthropic))).name,
"Erens Org"
);
let files: Vec<OsString> = okv(fs::read_dir(&dir.path))
.filter_map(Result::ok)
.map(|entry| entry.file_name())
.collect();
assert_eq!(files, vec![OsString::from("connections.json")]);
let raw = okv(fs::read_to_string(®_path));
assert!(
!raw.contains("sk-ant-admin"),
"no secret in registry: {raw}"
);
assert!(String::from_utf8_lossy(&out).contains("Connected anthropic"));
}
#[test]
fn connect_warning_is_shown_for_real_vendors_and_absent_for_gemini() {
for vendor in [ApiVendor::Anthropic, ApiVendor::OpenAI] {
let mut out = Vec::new();
okv(print_connect_warning(&mut out, style(), vendor));
let text = String::from_utf8_lossy(&out);
assert!(
text.contains("organization-wide") && text.contains("revoke"),
"{vendor} connect must warn about the org-wide admin key + revocation: {text}"
);
assert!(
text.is_ascii(),
"plain connect warning must be ASCII: {text}"
);
}
let mut out = Vec::new();
okv(gemini_connect(&mut out, style()));
assert!(
!String::from_utf8_lossy(&out).contains("organization-wide"),
"gemini connect must not show the admin-key warning"
);
}
#[test]
fn org_label_from_server_is_sanitized_and_renders_safely() {
let label = OrgLabel::from_server(
"\u{1b}[31mEvil\u{1b}[0m Café",
Some("org-\u{1b}123".to_string()),
);
assert!(!label.name.chars().any(|c| c.is_control()));
assert!(label.name.contains("Café"));
assert!(!somev(label.id.as_ref()).chars().any(|c| c.is_control()));
let mut out = Vec::new();
okv(emit(
&mut out,
style(),
&connected_message(ApiVendor::Anthropic, Some(&label)),
));
let text = String::from_utf8_lossy(&out);
assert!(text.is_ascii(), "plain output must be ASCII: {text}");
assert!(!text.contains('\u{1b}'), "no escape leaks: {text}");
}
#[test]
fn connect_openai_probes_costs_endpoint_and_stores_on_200() {
install_mock_keychain();
let store = okv(CredentialStore::new());
let dir = TempDir::new("connect-openai");
let registry = ConnectionRegistry::at(dir.path.join("connections.json"));
let adapters = LoopbackAdapters {
server: serve_sequence(vec![ok_json(EMPTY_COSTS)]),
};
let key = SecretString::from("sk-admin-FAKE0002".to_string());
let mut out = Vec::new();
let code = okv(run_connect(
ApiVendor::OpenAI,
key,
&adapters,
&store,
®istry,
&mut out,
style(),
));
assert_eq!(code, 0);
assert!(somev(okv(store.retrieve(ApiVendor::OpenAI)))
.expose_secret()
.starts_with("sk-admin-"));
assert!(okv(registry.is_connected(ApiVendor::OpenAI)));
let request = okv(adapters.server.next_request());
assert!(
request.contains("GET /v1/organization/costs"),
"the OpenAI probe must hit /costs: {request}"
);
assert!(String::from_utf8_lossy(&out).contains("Connected openai"));
}
#[test]
fn a_rejected_key_is_not_stored() {
install_mock_keychain();
let store = okv(CredentialStore::new());
let dir = TempDir::new("connect-rejected");
let registry = ConnectionRegistry::at(dir.path.join("connections.json"));
let adapters = LoopbackAdapters {
server: serve_sequence(vec![reply("401 Unauthorized", &[], "{}")]),
};
let key = SecretString::from("sk-ant-admin-REJECTED".to_string());
let mut out = Vec::new();
let code = okv(run_connect(
ApiVendor::Anthropic,
key,
&adapters,
&store,
®istry,
&mut out,
style(),
));
assert_eq!(code, 1);
assert!(
okv(store.retrieve(ApiVendor::Anthropic)).is_none(),
"a rejected key must NOT be stored"
);
assert!(!okv(registry.is_connected(ApiVendor::Anthropic)));
assert!(String::from_utf8_lossy(&out)
.to_lowercase()
.contains("rejected"));
}
#[test]
fn a_wrong_class_key_is_refused_without_any_request() {
install_mock_keychain();
let store = okv(CredentialStore::new());
let dir = TempDir::new("connect-wrongclass");
let registry = ConnectionRegistry::at(dir.path.join("connections.json"));
let adapters = LoopbackAdapters {
server: serve_sequence(vec![]),
};
let key = SecretString::from("sk-ant-api03-not-admin-secret".to_string());
let mut out = Vec::new();
let code = okv(run_connect(
ApiVendor::Anthropic,
key,
&adapters,
&store,
®istry,
&mut out,
style(),
));
assert_eq!(code, 1);
assert!(okv(store.retrieve(ApiVendor::Anthropic)).is_none());
let rendered = String::from_utf8_lossy(&out);
assert!(
rendered.contains("sk-ant-admin"),
"expected-prefix remediation: {rendered}"
);
assert!(
!rendered.contains("not-admin-secret"),
"the key must not be echoed: {rendered}"
);
assert!(
rendered.is_ascii(),
"ascii-mode output must be ASCII: {rendered}"
);
}
#[test]
fn disconnect_removes_key_registry_entry_and_label() {
install_mock_keychain();
let store = okv(CredentialStore::new());
let dir = TempDir::new("disconnect");
let registry = ConnectionRegistry::at(dir.path.join("connections.json"));
let adapters = LoopbackAdapters {
server: serve_sequence(vec![ok_json(ME_BODY)]),
};
let _ = okv(run_connect(
ApiVendor::Anthropic,
SecretString::from("sk-ant-admin-FAKE0003".to_string()),
&adapters,
&store,
®istry,
&mut Vec::new(),
style(),
));
assert!(okv(store.retrieve(ApiVendor::Anthropic)).is_some());
let mut out = Vec::new();
let code = okv(run_disconnect(
ApiVendor::Anthropic,
&store,
®istry,
&mut out,
style(),
));
assert_eq!(code, 0);
assert!(okv(store.retrieve(ApiVendor::Anthropic)).is_none());
assert!(!okv(registry.is_connected(ApiVendor::Anthropic)));
assert!(okv(registry.label(ApiVendor::Anthropic)).is_none());
assert!(String::from_utf8_lossy(&out).contains("Disconnected anthropic"));
}
#[test]
fn disconnect_is_idempotent_with_nothing_stored() {
install_mock_keychain();
let store = okv(CredentialStore::new());
let dir = TempDir::new("disconnect-empty");
let registry = ConnectionRegistry::at(dir.path.join("connections.json"));
let mut out = Vec::new();
let code = okv(run_disconnect(
ApiVendor::OpenAI,
&store,
®istry,
&mut out,
style(),
));
assert_eq!(code, 0);
assert!(String::from_utf8_lossy(&out).contains("Disconnected openai"));
}
#[test]
fn connections_lists_local_state_without_network() {
install_mock_keychain();
let store = okv(CredentialStore::new());
let dir = TempDir::new("connections-list");
let registry = ConnectionRegistry::at(dir.path.join("connections.json"));
let _ = okv(run_connect(
ApiVendor::Anthropic,
SecretString::from("sk-ant-admin-FAKE0004".to_string()),
&LoopbackAdapters {
server: serve_sequence(vec![ok_json(ME_BODY)]),
},
&store,
®istry,
&mut Vec::new(),
style(),
));
let dummy = LoopbackAdapters {
server: serve_sequence(vec![]),
};
let mut out = Vec::new();
let code = okv(run_connections(
false,
&dummy,
&store,
®istry,
&mut out,
style(),
));
assert_eq!(code, 0);
let rendered = String::from_utf8_lossy(&out);
assert!(rendered.contains("anthropic") && rendered.contains("connected"));
assert!(rendered.contains("Erens Org"));
assert!(rendered.contains("openai") && rendered.contains("not connected"));
assert!(
rendered.contains("gemini") && rendered.contains("no sanctioned static-key usage API")
);
assert!(
!rendered.contains('—'),
"em-dash must be folded in ascii mode: {rendered}"
);
}
#[test]
fn connections_check_revalidates_a_connected_vendor() {
install_mock_keychain();
let store = okv(CredentialStore::new());
let dir = TempDir::new("connections-check");
let registry = ConnectionRegistry::at(dir.path.join("connections.json"));
let _ = okv(run_connect(
ApiVendor::Anthropic,
SecretString::from("sk-ant-admin-FAKE0005".to_string()),
&LoopbackAdapters {
server: serve_sequence(vec![ok_json(ME_BODY)]),
},
&store,
®istry,
&mut Vec::new(),
style(),
));
let mut out = Vec::new();
let code = okv(run_connections(
true,
&LoopbackAdapters {
server: serve_sequence(vec![ok_json(ME_BODY)]),
},
&store,
®istry,
&mut out,
style(),
));
assert_eq!(code, 0);
assert!(String::from_utf8_lossy(&out).contains("verified just now"));
}
#[test]
fn connections_check_surfaces_a_failed_revalidation() {
install_mock_keychain();
let store = okv(CredentialStore::new());
let dir = TempDir::new("connections-check-fail");
let registry = ConnectionRegistry::at(dir.path.join("connections.json"));
let _ = okv(run_connect(
ApiVendor::Anthropic,
SecretString::from("sk-ant-admin-FAKE0006".to_string()),
&LoopbackAdapters {
server: serve_sequence(vec![ok_json(ME_BODY)]),
},
&store,
®istry,
&mut Vec::new(),
style(),
));
let mut out = Vec::new();
let code = okv(run_connections(
true,
&LoopbackAdapters {
server: serve_sequence(vec![reply("401 Unauthorized", &[], "{}")]),
},
&store,
®istry,
&mut out,
style(),
));
assert_eq!(code, 0);
let rendered = String::from_utf8_lossy(&out);
assert!(
rendered.contains("check failed"),
"a failed re-validation must be surfaced as text: {rendered}"
);
assert!(
rendered.is_ascii(),
"ascii-mode output must be ASCII: {rendered}"
);
}
#[test]
fn connections_check_revalidates_a_connected_openai_vendor() {
install_mock_keychain();
let store = okv(CredentialStore::new());
let dir = TempDir::new("connections-check-openai");
let registry = ConnectionRegistry::at(dir.path.join("connections.json"));
let _ = okv(run_connect(
ApiVendor::OpenAI,
SecretString::from("sk-admin-FAKE0007".to_string()),
&LoopbackAdapters {
server: serve_sequence(vec![ok_json(EMPTY_COSTS)]),
},
&store,
®istry,
&mut Vec::new(),
style(),
));
let mut out = Vec::new();
let code = okv(run_connections(
true,
&LoopbackAdapters {
server: serve_sequence(vec![ok_json(EMPTY_COSTS)]),
},
&store,
®istry,
&mut out,
style(),
));
assert_eq!(code, 0);
assert!(String::from_utf8_lossy(&out).contains("verified just now"));
}
#[test]
fn gemini_connect_prints_unavailable_and_stores_nothing() {
let mut out = Vec::new();
let code = okv(gemini_connect(&mut out, style()));
assert_eq!(code, 0);
let rendered = String::from_utf8_lossy(&out);
assert!(rendered.contains("no sanctioned static-key usage API"));
assert!(
rendered.is_ascii(),
"ascii-mode output must be ASCII: {rendered}"
);
}
}