use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::client::Client;
use crate::error::Result;
use crate::pagination::Paginated;
use super::MANAGED_AGENTS_BETA;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Vault {
pub id: String,
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub ty: Option<String>,
pub display_name: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub archived_at: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct CreateVaultRequest {
pub display_name: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, String>,
}
impl CreateVaultRequest {
#[must_use]
pub fn new(display_name: impl Into<String>) -> Self {
Self {
display_name: display_name.into(),
metadata: HashMap::new(),
}
}
#[must_use]
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ListVaultsParams {
pub after: Option<String>,
pub before: Option<String>,
pub limit: Option<u32>,
pub include_archived: Option<bool>,
}
impl ListVaultsParams {
fn to_query(&self) -> Vec<(&'static str, String)> {
let mut q = Vec::new();
if let Some(a) = &self.after {
q.push(("after", a.clone()));
}
if let Some(b) = &self.before {
q.push(("before", b.clone()));
}
if let Some(l) = self.limit {
q.push(("limit", l.to_string()));
}
if let Some(ia) = self.include_archived {
q.push(("include_archived", ia.to_string()));
}
q
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum TokenEndpointAuth {
None,
ClientSecretBasic {
client_secret: String,
},
ClientSecretPost {
client_secret: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OAuthRefresh {
pub token_endpoint: String,
pub client_id: String,
pub refresh_token: String,
pub token_endpoint_auth: TokenEndpointAuth,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CredentialAuth {
McpOauth(McpOauthAuth),
StaticBearer(StaticBearerAuth),
Other(serde_json::Value),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct McpOauthAuth {
pub mcp_server_url: String,
pub access_token: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refresh: Option<OAuthRefresh>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct StaticBearerAuth {
pub mcp_server_url: String,
pub token: String,
}
const KNOWN_CREDENTIAL_AUTH_TAGS: &[&str] = &["mcp_oauth", "static_bearer"];
impl Serialize for CredentialAuth {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
match self {
Self::McpOauth(v) => {
use serde::ser::SerializeMap;
let mut map = s.serialize_map(None)?;
map.serialize_entry("type", "mcp_oauth")?;
map.serialize_entry("mcp_server_url", &v.mcp_server_url)?;
map.serialize_entry("access_token", &v.access_token)?;
if let Some(e) = &v.expires_at {
map.serialize_entry("expires_at", e)?;
}
if let Some(r) = &v.refresh {
map.serialize_entry("refresh", r)?;
}
map.end()
}
Self::StaticBearer(v) => {
use serde::ser::SerializeMap;
let mut map = s.serialize_map(None)?;
map.serialize_entry("type", "static_bearer")?;
map.serialize_entry("mcp_server_url", &v.mcp_server_url)?;
map.serialize_entry("token", &v.token)?;
map.end()
}
Self::Other(v) => v.serialize(s),
}
}
}
impl<'de> Deserialize<'de> for CredentialAuth {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
let raw = serde_json::Value::deserialize(d)?;
let tag = raw.get("type").and_then(serde_json::Value::as_str);
match tag {
Some("mcp_oauth") if KNOWN_CREDENTIAL_AUTH_TAGS.contains(&"mcp_oauth") => {
let body = serde_json::from_value::<McpOauthAuth>(raw)
.map_err(serde::de::Error::custom)?;
Ok(Self::McpOauth(body))
}
Some("static_bearer") if KNOWN_CREDENTIAL_AUTH_TAGS.contains(&"static_bearer") => {
let body = serde_json::from_value::<StaticBearerAuth>(raw)
.map_err(serde::de::Error::custom)?;
Ok(Self::StaticBearer(body))
}
_ => Ok(Self::Other(raw)),
}
}
}
impl CredentialAuth {
#[must_use]
pub fn mcp_oauth(
mcp_server_url: impl Into<String>,
access_token: impl Into<String>,
) -> McpOauthBuilder {
McpOauthBuilder {
mcp_server_url: mcp_server_url.into(),
access_token: access_token.into(),
expires_at: None,
refresh: None,
}
}
#[must_use]
pub fn static_bearer(mcp_server_url: impl Into<String>, token: impl Into<String>) -> Self {
Self::StaticBearer(StaticBearerAuth {
mcp_server_url: mcp_server_url.into(),
token: token.into(),
})
}
}
#[derive(Debug, Clone)]
pub struct McpOauthBuilder {
mcp_server_url: String,
access_token: String,
expires_at: Option<String>,
refresh: Option<OAuthRefresh>,
}
impl McpOauthBuilder {
#[must_use]
pub fn expires_at(mut self, when: impl Into<String>) -> Self {
self.expires_at = Some(when.into());
self
}
#[must_use]
pub fn refresh(mut self, refresh: OAuthRefresh) -> Self {
self.refresh = Some(refresh);
self
}
#[must_use]
pub fn build(self) -> CredentialAuth {
CredentialAuth::McpOauth(McpOauthAuth {
mcp_server_url: self.mcp_server_url,
access_token: self.access_token,
expires_at: self.expires_at,
refresh: self.refresh,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Credential {
pub id: String,
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub ty: Option<String>,
pub vault_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth: Option<CredentialAuthResponse>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub archived_at: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum CredentialAuthResponse {
McpOauth {
mcp_server_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
expires_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
refresh: Option<serde_json::Value>,
},
StaticBearer {
mcp_server_url: String,
},
#[serde(other)]
Other,
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct CreateCredentialRequest {
pub auth: CredentialAuth,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
}
impl CreateCredentialRequest {
#[must_use]
pub fn new(auth: CredentialAuth) -> Self {
Self {
auth,
display_name: None,
}
}
#[must_use]
pub fn with_display_name(mut self, name: impl Into<String>) -> Self {
self.display_name = Some(name.into());
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[non_exhaustive]
pub struct UpdateCredentialRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub auth: Option<CredentialAuthPatch>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum CredentialAuthPatch {
McpOauth {
#[serde(skip_serializing_if = "Option::is_none")]
access_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
expires_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
refresh: Option<OAuthRefreshPatch>,
},
StaticBearer {
#[serde(skip_serializing_if = "Option::is_none")]
token: Option<String>,
},
}
#[derive(Debug, Clone, Default, Serialize)]
#[non_exhaustive]
pub struct OAuthRefreshPatch {
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_endpoint_auth: Option<TokenEndpointAuth>,
}
pub struct Vaults<'a> {
client: &'a Client,
}
impl<'a> Vaults<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self { client }
}
pub async fn create(&self, request: CreateVaultRequest) -> Result<Vault> {
let body = &request;
self.client
.execute_with_retry(
|| {
self.client
.request_builder(reqwest::Method::POST, "/v1/vaults")
.json(body)
},
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn retrieve(&self, vault_id: &str) -> Result<Vault> {
let path = format!("/v1/vaults/{vault_id}");
self.client
.execute_with_retry(
|| self.client.request_builder(reqwest::Method::GET, &path),
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn list(&self, params: ListVaultsParams) -> Result<Paginated<Vault>> {
let query = params.to_query();
self.client
.execute_with_retry(
|| {
let mut req = self
.client
.request_builder(reqwest::Method::GET, "/v1/vaults");
for (k, v) in &query {
req = req.query(&[(k, v)]);
}
req
},
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn archive(&self, vault_id: &str) -> Result<Vault> {
let path = format!("/v1/vaults/{vault_id}/archive");
self.client
.execute_with_retry(
|| self.client.request_builder(reqwest::Method::POST, &path),
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn delete(&self, vault_id: &str) -> Result<()> {
let path = format!("/v1/vaults/{vault_id}");
let _: serde_json::Value = self
.client
.execute_with_retry(
|| self.client.request_builder(reqwest::Method::DELETE, &path),
&[MANAGED_AGENTS_BETA],
)
.await?;
Ok(())
}
#[must_use]
pub fn credentials(&self, vault_id: impl Into<String>) -> Credentials<'_> {
Credentials {
client: self.client,
vault_id: vault_id.into(),
}
}
}
pub struct Credentials<'a> {
client: &'a Client,
vault_id: String,
}
impl Credentials<'_> {
pub async fn create(&self, request: CreateCredentialRequest) -> Result<Credential> {
let path = format!("/v1/vaults/{}/credentials", self.vault_id);
let body = &request;
self.client
.execute_with_retry(
|| {
self.client
.request_builder(reqwest::Method::POST, &path)
.json(body)
},
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn update(
&self,
credential_id: &str,
request: UpdateCredentialRequest,
) -> Result<Credential> {
let path = format!("/v1/vaults/{}/credentials/{credential_id}", self.vault_id);
let body = &request;
self.client
.execute_with_retry(
|| {
self.client
.request_builder(reqwest::Method::POST, &path)
.json(body)
},
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn retrieve(&self, credential_id: &str) -> Result<Credential> {
let path = format!("/v1/vaults/{}/credentials/{credential_id}", self.vault_id);
self.client
.execute_with_retry(
|| self.client.request_builder(reqwest::Method::GET, &path),
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn list(&self, params: ListVaultsParams) -> Result<Paginated<Credential>> {
let path = format!("/v1/vaults/{}/credentials", self.vault_id);
let query = params.to_query();
self.client
.execute_with_retry(
|| {
let mut req = self.client.request_builder(reqwest::Method::GET, &path);
for (k, v) in &query {
req = req.query(&[(k, v)]);
}
req
},
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn archive(&self, credential_id: &str) -> Result<Credential> {
let path = format!(
"/v1/vaults/{}/credentials/{credential_id}/archive",
self.vault_id
);
self.client
.execute_with_retry(
|| self.client.request_builder(reqwest::Method::POST, &path),
&[MANAGED_AGENTS_BETA],
)
.await
}
pub async fn delete(&self, credential_id: &str) -> Result<()> {
let path = format!("/v1/vaults/{}/credentials/{credential_id}", self.vault_id);
let _: serde_json::Value = self
.client
.execute_with_retry(
|| self.client.request_builder(reqwest::Method::DELETE, &path),
&[MANAGED_AGENTS_BETA],
)
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
use wiremock::matchers::{body_partial_json, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn client_for(mock: &MockServer) -> Client {
Client::builder()
.api_key("sk-ant-test")
.base_url(mock.uri())
.build()
.unwrap()
}
#[test]
fn mcp_oauth_round_trips_via_credential_auth() {
let auth = CredentialAuth::mcp_oauth("https://mcp.slack.com/mcp", "xoxp-token")
.expires_at("2026-04-15T00:00:00Z")
.refresh(OAuthRefresh {
token_endpoint: "https://slack.com/api/oauth.v2.access".into(),
client_id: "1234567890".into(),
refresh_token: "xoxe-refresh".into(),
token_endpoint_auth: TokenEndpointAuth::ClientSecretPost {
client_secret: "abc123".into(),
},
scope: Some("channels:read".into()),
})
.build();
let v = serde_json::to_value(&auth).unwrap();
assert_eq!(v["type"], "mcp_oauth");
assert_eq!(v["mcp_server_url"], "https://mcp.slack.com/mcp");
assert_eq!(v["access_token"], "xoxp-token");
assert_eq!(v["refresh"]["client_id"], "1234567890");
assert_eq!(
v["refresh"]["token_endpoint_auth"]["type"],
"client_secret_post"
);
}
#[test]
fn static_bearer_round_trips() {
let auth = CredentialAuth::static_bearer("https://mcp.linear.app/mcp", "lin_api_x");
let v = serde_json::to_value(&auth).unwrap();
assert_eq!(
v,
json!({
"type": "static_bearer",
"mcp_server_url": "https://mcp.linear.app/mcp",
"token": "lin_api_x"
})
);
}
#[test]
fn unknown_auth_type_falls_through_to_other() {
let raw = json!({"type": "future_auth", "x": 1});
let parsed: CredentialAuth = serde_json::from_value(raw.clone()).unwrap();
match parsed {
CredentialAuth::Other(v) => assert_eq!(v, raw),
CredentialAuth::McpOauth(_) | CredentialAuth::StaticBearer(_) => {
panic!("expected Other")
}
}
}
#[tokio::test]
async fn create_vault_posts_with_metadata() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/vaults"))
.and(body_partial_json(json!({
"display_name": "Alice",
"metadata": {"external_user_id": "usr_abc"}
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "vlt_01",
"type": "vault",
"display_name": "Alice",
"metadata": {"external_user_id": "usr_abc"},
"created_at": "2026-04-30T12:00:00Z"
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let req = CreateVaultRequest::new("Alice").with_metadata("external_user_id", "usr_abc");
let v = client.managed_agents().vaults().create(req).await.unwrap();
assert_eq!(v.id, "vlt_01");
assert_eq!(
v.metadata.get("external_user_id").map(String::as_str),
Some("usr_abc")
);
}
#[tokio::test]
async fn create_credential_serializes_static_bearer_body() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/vaults/vlt_01/credentials"))
.and(body_partial_json(json!({
"auth": {
"type": "static_bearer",
"mcp_server_url": "https://mcp.linear.app/mcp",
"token": "lin_api_x"
},
"display_name": "Linear API key"
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "cred_01",
"type": "credential",
"vault_id": "vlt_01",
"auth": {
"type": "static_bearer",
"mcp_server_url": "https://mcp.linear.app/mcp"
}
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let req = CreateCredentialRequest::new(CredentialAuth::static_bearer(
"https://mcp.linear.app/mcp",
"lin_api_x",
))
.with_display_name("Linear API key");
let c = client
.managed_agents()
.vaults()
.credentials("vlt_01")
.create(req)
.await
.unwrap();
assert_eq!(c.id, "cred_01");
}
#[tokio::test]
async fn update_credential_rotates_token_via_patch() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/vaults/vlt_01/credentials/cred_01"))
.and(body_partial_json(json!({
"auth": {
"type": "mcp_oauth",
"access_token": "xoxp-new",
"refresh": {"refresh_token": "xoxe-new"}
}
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "cred_01",
"type": "credential",
"vault_id": "vlt_01",
"auth": {
"type": "mcp_oauth",
"mcp_server_url": "https://mcp.slack.com/mcp"
}
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let patch = UpdateCredentialRequest {
auth: Some(CredentialAuthPatch::McpOauth {
access_token: Some("xoxp-new".into()),
expires_at: None,
refresh: Some(OAuthRefreshPatch {
refresh_token: Some("xoxe-new".into()),
..Default::default()
}),
}),
display_name: None,
};
client
.managed_agents()
.vaults()
.credentials("vlt_01")
.update("cred_01", patch)
.await
.unwrap();
}
#[tokio::test]
async fn retrieve_vault_returns_typed_record() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/vaults/vlt_01"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "vlt_01",
"type": "vault",
"display_name": "Alice"
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let v = client
.managed_agents()
.vaults()
.retrieve("vlt_01")
.await
.unwrap();
assert_eq!(v.id, "vlt_01");
}
#[tokio::test]
async fn list_vaults_passes_pagination_query_params() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/vaults"))
.and(wiremock::matchers::query_param("limit", "10"))
.and(wiremock::matchers::query_param("after", "vlt_x"))
.and(wiremock::matchers::query_param("include_archived", "true"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [{"id": "vlt_01", "display_name": "Alice"}],
"has_more": false
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let page = client
.managed_agents()
.vaults()
.list(ListVaultsParams {
after: Some("vlt_x".into()),
limit: Some(10),
include_archived: Some(true),
..Default::default()
})
.await
.unwrap();
assert_eq!(page.data.len(), 1);
}
#[tokio::test]
async fn archive_vault_posts_to_archive_subpath() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/vaults/vlt_01/archive"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "vlt_01",
"display_name": "Alice",
"archived_at": "2026-04-30T12:00:00Z"
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let v = client
.managed_agents()
.vaults()
.archive("vlt_01")
.await
.unwrap();
assert!(v.archived_at.is_some());
}
#[tokio::test]
async fn delete_vault_returns_unit() {
let mock = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/v1/vaults/vlt_01"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.mount(&mock)
.await;
let client = client_for(&mock);
client
.managed_agents()
.vaults()
.delete("vlt_01")
.await
.unwrap();
}
#[tokio::test]
async fn retrieve_credential_returns_record_without_secrets() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/vaults/vlt_01/credentials/cred_01"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "cred_01",
"type": "credential",
"vault_id": "vlt_01",
"display_name": "Linear",
"auth": {
"type": "static_bearer",
"mcp_server_url": "https://mcp.linear.app/mcp"
}
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let c = client
.managed_agents()
.vaults()
.credentials("vlt_01")
.retrieve("cred_01")
.await
.unwrap();
assert_eq!(c.vault_id, "vlt_01");
match c.auth.unwrap() {
CredentialAuthResponse::StaticBearer { mcp_server_url } => {
assert_eq!(mcp_server_url, "https://mcp.linear.app/mcp");
}
_ => panic!("expected StaticBearer auth"),
}
}
#[tokio::test]
async fn list_credentials_paginates_under_vault() {
let mock = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/vaults/vlt_01/credentials"))
.and(wiremock::matchers::query_param("limit", "5"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [{"id": "cred_01", "vault_id": "vlt_01"}],
"has_more": false
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let page = client
.managed_agents()
.vaults()
.credentials("vlt_01")
.list(ListVaultsParams {
limit: Some(5),
..Default::default()
})
.await
.unwrap();
assert_eq!(page.data.len(), 1);
}
#[tokio::test]
async fn delete_credential_returns_unit() {
let mock = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/v1/vaults/vlt_01/credentials/cred_01"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.mount(&mock)
.await;
let client = client_for(&mock);
client
.managed_agents()
.vaults()
.credentials("vlt_01")
.delete("cred_01")
.await
.unwrap();
}
#[test]
fn token_endpoint_auth_round_trips_all_three_variants() {
for auth in [
TokenEndpointAuth::None,
TokenEndpointAuth::ClientSecretBasic {
client_secret: "abc".into(),
},
TokenEndpointAuth::ClientSecretPost {
client_secret: "def".into(),
},
] {
let v = serde_json::to_value(&auth).unwrap();
let parsed: TokenEndpointAuth = serde_json::from_value(v).unwrap();
assert_eq!(parsed, auth);
}
}
#[test]
fn credential_auth_response_round_trips_known_variants() {
let oauth = CredentialAuthResponse::McpOauth {
mcp_server_url: "https://mcp.x/mcp".into(),
expires_at: Some("2026-05-01T00:00:00Z".into()),
refresh: None,
};
let v = serde_json::to_value(&oauth).unwrap();
assert_eq!(v["type"], "mcp_oauth");
assert_eq!(v["mcp_server_url"], "https://mcp.x/mcp");
let parsed: CredentialAuthResponse = serde_json::from_value(v).unwrap();
assert_eq!(parsed, oauth);
let bearer = CredentialAuthResponse::StaticBearer {
mcp_server_url: "https://mcp.y/mcp".into(),
};
let v = serde_json::to_value(&bearer).unwrap();
assert_eq!(
v,
json!({"type": "static_bearer", "mcp_server_url": "https://mcp.y/mcp"})
);
let parsed: CredentialAuthResponse = serde_json::from_value(v).unwrap();
assert_eq!(parsed, bearer);
let unknown: CredentialAuthResponse =
serde_json::from_value(json!({"type": "future_kind", "x": 1})).unwrap();
assert!(matches!(unknown, CredentialAuthResponse::Other));
}
#[tokio::test]
async fn archive_credential_posts_to_archive_subpath() {
let mock = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/vaults/vlt_01/credentials/cred_01/archive"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "cred_01",
"type": "credential",
"vault_id": "vlt_01",
"archived_at": "2026-04-30T12:00:00Z"
})))
.mount(&mock)
.await;
let client = client_for(&mock);
let c = client
.managed_agents()
.vaults()
.credentials("vlt_01")
.archive("cred_01")
.await
.unwrap();
assert!(c.archived_at.is_some());
}
}