use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireAuthBindingRef {
pub realm: meerkat_core::connection::RealmId,
pub binding: meerkat_core::connection::BindingId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<meerkat_core::connection::ProfileId>,
}
impl From<meerkat_core::AuthBindingRef> for WireAuthBindingRef {
fn from(value: meerkat_core::AuthBindingRef) -> Self {
Self {
realm: value.realm,
binding: value.binding,
profile: value.profile,
}
}
}
impl From<WireAuthBindingRef> for meerkat_core::AuthBindingRef {
fn from(value: WireAuthBindingRef) -> Self {
Self {
realm: value.realm,
binding: value.binding,
profile: value.profile,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealmIdParams {
pub realm_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct BindingIdParams {
pub realm_id: String,
pub binding_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct CreateProfileParams {
pub realm_id: String,
pub binding_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_id: Option<String>,
pub auth_method: String,
pub secret: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LoginStartParams {
pub provider: String,
pub redirect_uri: String,
pub realm_id: String,
pub binding_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LoginCompleteParams {
pub provider: String,
pub code: String,
pub state: String,
pub redirect_uri: String,
pub realm_id: String,
pub binding_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DeviceStartParams {
pub provider: String,
pub realm_id: String,
pub binding_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DeviceCompleteParams {
pub provider: String,
pub device_code: String,
pub realm_id: String,
pub binding_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ProvisionApiKeyParams {
pub access_token: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub realm_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub binding_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireBackendProfile {
pub id: String,
pub provider: String,
pub backend_kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub options: serde_json::Value,
}
impl From<&meerkat_core::BackendProfile> for WireBackendProfile {
fn from(value: &meerkat_core::BackendProfile) -> Self {
Self {
id: value.id.clone(),
provider: value.provider.as_str().to_string(),
backend_kind: value.backend_kind.clone(),
base_url: value.base_url.clone(),
options: value.options.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireAuthProfile {
pub id: String,
pub provider: String,
pub auth_method: String,
pub source_kind: String,
}
impl From<&meerkat_core::AuthProfile> for WireAuthProfile {
fn from(value: &meerkat_core::AuthProfile) -> Self {
Self {
id: value.id.clone(),
provider: value.provider.as_str().to_string(),
auth_method: value.auth_method.clone(),
source_kind: value.source.kind_label().to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireProviderBinding {
pub id: String,
pub backend_profile: String,
pub auth_profile: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_model: Option<String>,
#[serde(default)]
pub allow_auth_override: bool,
#[serde(default)]
pub require_metadata_account: bool,
#[serde(default)]
pub require_metadata_workspace: bool,
}
impl From<&meerkat_core::ProviderBinding> for WireProviderBinding {
fn from(value: &meerkat_core::ProviderBinding) -> Self {
Self {
id: value.id.clone(),
backend_profile: value.backend_profile.clone(),
auth_profile: value.auth_profile.clone(),
default_model: value.default_model.clone(),
allow_auth_override: value.policy.allow_auth_override,
require_metadata_account: value.policy.require_metadata_account,
require_metadata_workspace: value.policy.require_metadata_workspace,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireRealmConnectionSet {
pub realm_id: String,
pub backends: BTreeMap<String, WireBackendProfile>,
pub auth_profiles: BTreeMap<String, WireAuthProfile>,
pub bindings: BTreeMap<String, WireProviderBinding>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_binding: Option<String>,
}
impl From<&meerkat_core::RealmConnectionSet> for WireRealmConnectionSet {
fn from(value: &meerkat_core::RealmConnectionSet) -> Self {
Self {
realm_id: value.realm_id.clone(),
backends: value
.backends
.iter()
.map(|(k, v)| (k.clone(), WireBackendProfile::from(v)))
.collect(),
auth_profiles: value
.auth_profiles
.iter()
.map(|(k, v)| (k.clone(), WireAuthProfile::from(v)))
.collect(),
bindings: value
.bindings
.iter()
.map(|(k, v)| (k.clone(), WireProviderBinding::from(v)))
.collect(),
default_binding: value.default_binding.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum WireAuthError {
MissingSecret,
UnsupportedCombination { backend: String, auth: String },
MissingRequiredMetadata { field: String },
WorkspaceMismatch,
Expired,
StaleCredential,
RefreshRequired,
LeaseAbsent,
UserReauthRequired,
RefreshFailed { detail: String },
InteractiveLoginRequired,
HostOwnedUnavailable,
Io { detail: String },
Other { detail: String },
}
impl From<meerkat_core::AuthError> for WireAuthError {
fn from(value: meerkat_core::AuthError) -> Self {
match value {
meerkat_core::AuthError::MissingSecret => Self::MissingSecret,
meerkat_core::AuthError::UnsupportedCombination { backend, auth } => {
Self::UnsupportedCombination { backend, auth }
}
meerkat_core::AuthError::MissingRequiredMetadata(field) => {
Self::MissingRequiredMetadata { field }
}
meerkat_core::AuthError::WorkspaceMismatch => Self::WorkspaceMismatch,
meerkat_core::AuthError::Expired => Self::Expired,
meerkat_core::AuthError::StaleCredential => Self::StaleCredential,
meerkat_core::AuthError::RefreshRequired => Self::RefreshRequired,
meerkat_core::AuthError::LeaseAbsent => Self::LeaseAbsent,
meerkat_core::AuthError::UserReauthRequired => Self::UserReauthRequired,
meerkat_core::AuthError::RefreshFailed(detail) => Self::RefreshFailed { detail },
meerkat_core::AuthError::InteractiveLoginRequired => Self::InteractiveLoginRequired,
meerkat_core::AuthError::HostOwnedUnavailable => Self::HostOwnedUnavailable,
meerkat_core::AuthError::Io(detail) => Self::Io { detail },
meerkat_core::AuthError::Other(detail) => Self::Other { detail },
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireAuthStatus {
pub profile_id: String,
pub provider: String,
pub auth_method: String,
pub state: meerkat_core::AuthStatusPhase,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
pub expires_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "schema", schemars(with = "Option<String>"))]
pub last_refresh_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_error: Option<WireAuthError>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireBindingIdentity {
pub realm_id: String,
pub binding_id: String,
pub auth_binding: WireAuthBindingRef,
}
impl From<&meerkat_core::AuthBindingRef> for WireBindingIdentity {
fn from(cref: &meerkat_core::AuthBindingRef) -> Self {
Self {
realm_id: cref.realm.to_string(),
binding_id: cref.binding.to_string(),
auth_binding: WireAuthBindingRef::from(cref.clone()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireAuthProfileCreated {
#[serde(flatten)]
pub identity: WireBindingIdentity,
pub profile_id: String,
pub provider: String,
pub auth_method: String,
pub stored: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireAuthProfileDetail {
pub auth_binding: WireAuthBindingRef,
pub binding_id: String,
pub profile_id: String,
pub auth_profile: WireAuthProfile,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireAuthProfileCleared {
#[serde(flatten)]
pub identity: WireBindingIdentity,
pub profile_id: String,
pub cleared: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireLoginStart {
pub authorize_url: String,
pub state: String,
pub redirect_uri: String,
pub provider: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireLoginReady {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
#[serde(flatten)]
pub identity: WireBindingIdentity,
pub profile_id: String,
pub provider: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
pub has_refresh_token: bool,
pub scopes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireDeviceStart {
pub device_code: String,
pub user_code: String,
pub verification_uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub verification_uri_complete: Option<String>,
pub expires_in: u64,
pub interval: u64,
pub provider: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum WireDeviceCompleteResult {
Pending,
SlowDown,
AccessDenied,
Expired,
Ready {
#[serde(flatten)]
identity: Box<WireBindingIdentity>,
profile_id: String,
provider: String,
#[serde(skip_serializing_if = "Option::is_none")]
expires_at: Option<String>,
has_refresh_token: bool,
scopes: Vec<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireProvisionApiKeyResult {
#[serde(flatten)]
pub identity: WireBindingIdentity,
pub profile_id: String,
pub provider: String,
pub auth_mode: String,
pub has_api_key: bool,
pub scopes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireRealmSummary {
pub realm_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_binding: Option<String>,
pub backend_count: usize,
pub auth_profile_count: usize,
pub binding_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireRealmList {
pub realms: Vec<WireRealmSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireAuthProfilesList {
pub realm_id: String,
pub auth_profiles: Vec<WireAuthProfile>,
pub backend_profiles: Vec<WireBackendProfile>,
pub bindings: Vec<WireProviderBinding>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct WireAuthStatusDetail {
#[serde(flatten)]
pub identity: WireBindingIdentity,
pub profile_id: String,
pub provider: String,
pub auth_method: String,
pub state: meerkat_core::AuthStatusPhase,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_refresh_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub account_id: Option<String>,
pub has_refresh_token: bool,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn auth_binding_roundtrip() {
let r = meerkat_core::AuthBindingRef {
realm: meerkat_core::connection::RealmId::parse("dev").unwrap(),
binding: meerkat_core::connection::BindingId::parse("default_openai").unwrap(),
profile: None,
};
let w: WireAuthBindingRef = r.clone().into();
assert_eq!(w.realm.as_str(), "dev");
assert_eq!(w.binding.as_str(), "default_openai");
assert!(w.profile.is_none());
let back: meerkat_core::AuthBindingRef = w.into();
assert_eq!(back, r);
}
#[test]
fn auth_binding_wire_json_has_no_string_form() {
let w = WireAuthBindingRef {
realm: meerkat_core::connection::RealmId::parse("prod").unwrap(),
binding: meerkat_core::connection::BindingId::parse("openai_main").unwrap(),
profile: Some(meerkat_core::connection::ProfileId::parse("ci").unwrap()),
};
let json = serde_json::to_string(&w).unwrap();
assert!(json.contains("\"realm\":\"prod\""));
assert!(json.contains("\"binding\":\"openai_main\""));
assert!(json.contains("\"profile\":\"ci\""));
assert!(!json.contains("prod:openai_main"));
}
#[test]
fn backend_profile_projects_provider_as_string() {
let bp = meerkat_core::BackendProfile {
id: "openai_api".into(),
provider: meerkat_core::Provider::OpenAI,
backend_kind: "openai_api".into(),
base_url: None,
options: serde_json::Value::Null,
};
let w: WireBackendProfile = (&bp).into();
assert_eq!(w.provider, "openai");
}
#[test]
fn auth_error_projects_to_tagged_variants() {
use meerkat_core::AuthError;
let e = AuthError::RefreshFailed("timeout".into());
let w: WireAuthError = e.into();
let s = serde_json::to_string(&w).unwrap();
assert!(s.contains("\"kind\":\"refresh_failed\""));
assert!(s.contains("\"detail\":\"timeout\""));
}
#[test]
fn device_complete_result_serializes_terminal_and_ready_shapes() {
let pending = serde_json::to_value(WireDeviceCompleteResult::Pending).unwrap();
assert_eq!(pending, serde_json::json!({ "state": "pending" }));
let cref = meerkat_core::AuthBindingRef {
realm: meerkat_core::connection::RealmId::parse("prod").unwrap(),
binding: meerkat_core::connection::BindingId::parse("anthropic_main").unwrap(),
profile: None,
};
let ready = serde_json::to_value(WireDeviceCompleteResult::Ready {
identity: Box::new(WireBindingIdentity::from(&cref)),
profile_id: "console".to_string(),
provider: "anthropic".to_string(),
expires_at: Some("2026-04-29T00:00:00Z".to_string()),
has_refresh_token: true,
scopes: vec!["org:create_api_key".to_string()],
})
.unwrap();
assert_eq!(ready["state"], "ready");
assert_eq!(ready["realm_id"], "prod");
assert_eq!(ready["binding_id"], "anthropic_main");
assert_eq!(ready["auth_binding"]["realm"], "prod");
assert_eq!(ready["auth_binding"]["binding"], "anthropic_main");
assert_eq!(ready["profile_id"], "console");
assert_eq!(ready["has_refresh_token"], true);
}
#[test]
fn provision_api_key_result_serializes_binding_identity() {
let cref = meerkat_core::AuthBindingRef {
realm: meerkat_core::connection::RealmId::parse("prod").unwrap(),
binding: meerkat_core::connection::BindingId::parse("anthropic_main").unwrap(),
profile: Some(meerkat_core::connection::ProfileId::parse("console").unwrap()),
};
let value = serde_json::to_value(WireProvisionApiKeyResult {
identity: WireBindingIdentity::from(&cref),
profile_id: "console".to_string(),
provider: "anthropic".to_string(),
auth_mode: "oauth_to_api_key".to_string(),
has_api_key: true,
scopes: vec!["user:profile".to_string()],
})
.unwrap();
assert_eq!(value["realm_id"], "prod");
assert_eq!(value["binding_id"], "anthropic_main");
assert_eq!(value["auth_binding"]["profile"], "console");
assert_eq!(value["auth_mode"], "oauth_to_api_key");
assert_eq!(value["has_api_key"], true);
}
#[test]
fn auth_status_serde_roundtrip() {
let status = WireAuthStatus {
profile_id: "p".into(),
provider: "openai".into(),
auth_method: "managed_chatgpt_oauth".into(),
state: meerkat_core::AuthStatusPhase::Valid,
expires_at: Some(chrono::Utc::now()),
last_refresh_at: None,
account_id: Some("acct_x".into()),
last_error: None,
};
let s = serde_json::to_string(&status).unwrap();
let back: WireAuthStatus = serde_json::from_str(&s).unwrap();
assert_eq!(back, status);
}
}