use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use crate::{ edition::{MachineAccessMode, OperationSecurityLevel}, ids::{AgentId, AuthProfileId, OperationId, SecretId, WorkspaceId}, protocol::AuthKind, }; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct BearerAuthConfig { pub header_name: String, pub secret_id: SecretId, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct BasicAuthConfig { pub username_secret_id: SecretId, pub password_secret_id: SecretId, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ApiKeyHeaderAuthConfig { pub header_name: String, pub secret_id: SecretId, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct ApiKeyQueryAuthConfig { pub param_name: String, pub secret_id: SecretId, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AuthConfig { Bearer(BearerAuthConfig), Basic(BasicAuthConfig), ApiKeyHeader(ApiKeyHeaderAuthConfig), ApiKeyQuery(ApiKeyQueryAuthConfig), } impl AuthConfig { pub fn secret_ids(&self) -> Vec<&SecretId> { match self { Self::Bearer(config) => vec![&config.secret_id], Self::Basic(config) => vec![&config.username_secret_id, &config.password_secret_id], Self::ApiKeyHeader(config) => vec![&config.secret_id], Self::ApiKeyQuery(config) => vec![&config.secret_id], } } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct AuthProfile { pub id: AuthProfileId, pub workspace_id: WorkspaceId, pub name: String, pub kind: AuthKind, pub config: AuthConfig, #[serde(with = "time::serde::rfc3339")] pub created_at: OffsetDateTime, #[serde(with = "time::serde::rfc3339")] pub updated_at: OffsetDateTime, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AgentTokenGrantType { AgentKey, RefreshToken, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct IssueAgentTokenRequest { pub grant_type: AgentTokenGrantType, #[serde(skip_serializing_if = "Option::is_none")] pub agent_key: Option, #[serde(skip_serializing_if = "Option::is_none")] pub refresh_token: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub scope: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct IssueOneTimeAgentTokenRequest { pub agent_key: String, pub operation_id: OperationId, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub scope: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct IssuedAgentTokenResponse { pub access_token: String, pub token_type: String, pub expires_in: u64, #[serde(skip_serializing_if = "Option::is_none")] pub refresh_token: Option, pub machine_access_mode: MachineAccessMode, pub security_level: OperationSecurityLevel, pub agent_id: AgentId, } #[cfg(test)] mod tests { use serde_json::json; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use super::{ AgentTokenGrantType, ApiKeyHeaderAuthConfig, AuthConfig, AuthProfile, IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest, IssuedAgentTokenResponse, }; use crate::{ edition::{MachineAccessMode, OperationSecurityLevel}, ids::{AuthProfileId, SecretId, WorkspaceId}, protocol::AuthKind, }; #[test] fn auth_profile_serializes_timestamps_as_rfc3339() { let profile = AuthProfile { id: AuthProfileId::new("auth_01"), workspace_id: WorkspaceId::new("ws_default"), name: "header".to_owned(), kind: AuthKind::ApiKeyHeader, config: AuthConfig::ApiKeyHeader(ApiKeyHeaderAuthConfig { header_name: "X-Api-Key".to_owned(), secret_id: SecretId::new("secret_01"), }), created_at: OffsetDateTime::parse("2026-03-25T12:00:00Z", &Rfc3339).unwrap(), updated_at: OffsetDateTime::parse("2026-03-25T12:05:00Z", &Rfc3339).unwrap(), }; let value = serde_json::to_value(&profile).unwrap(); assert_eq!(value["created_at"], json!("2026-03-25T12:00:00Z")); assert_eq!(value["updated_at"], json!("2026-03-25T12:05:00Z")); } #[test] fn issue_agent_token_request_serializes_stable_contract() { let request = IssueAgentTokenRequest { grant_type: AgentTokenGrantType::AgentKey, agent_key: Some("crk_agent_secret".to_owned()), refresh_token: None, scope: vec!["tools:call".to_owned()], }; let value = serde_json::to_value(&request).unwrap(); assert_eq!(value["grant_type"], json!("agent_key")); assert_eq!(value["agent_key"], json!("crk_agent_secret")); assert_eq!(value["scope"], json!(["tools:call"])); assert_eq!(value.get("refresh_token"), None); } #[test] fn issue_one_time_agent_token_request_serializes_stable_contract() { let request = IssueOneTimeAgentTokenRequest { agent_key: "crk_agent_secret".to_owned(), operation_id: "op_01".into(), scope: vec!["tools:call".to_owned()], }; let value = serde_json::to_value(&request).unwrap(); assert_eq!(value["agent_key"], json!("crk_agent_secret")); assert_eq!(value["operation_id"], json!("op_01")); assert_eq!(value["scope"], json!(["tools:call"])); } #[test] fn issued_agent_token_response_serializes_machine_access_metadata() { let response = IssuedAgentTokenResponse { access_token: "tok_01".to_owned(), token_type: "Bearer".to_owned(), expires_in: 300, refresh_token: Some("rfr_01".to_owned()), machine_access_mode: MachineAccessMode::ShortLivedToken, security_level: OperationSecurityLevel::Elevated, agent_id: "agt_01".into(), }; let value = serde_json::to_value(&response).unwrap(); assert_eq!(value["access_token"], json!("tok_01")); assert_eq!(value["token_type"], json!("Bearer")); assert_eq!(value["expires_in"], json!(300)); assert_eq!(value["refresh_token"], json!("rfr_01")); assert_eq!(value["machine_access_mode"], json!("short_lived_token")); assert_eq!(value["security_level"], json!("elevated")); assert_eq!(value["agent_id"], json!("agt_01")); } }