1use serde::{Deserialize, Serialize};
8
9use crate::client::Client;
10use crate::error::Result;
11use crate::pagination::Paginated;
12
13use super::ListParams;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18#[non_exhaustive]
19pub enum ApiKeyStatus {
20 Active,
22 Inactive,
24 Archived,
26 Expired,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33#[non_exhaustive]
34pub enum WriteApiKeyStatus {
35 Active,
37 Inactive,
39 Archived,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45#[non_exhaustive]
46pub struct ApiKeyCreator {
47 pub id: String,
49 #[serde(rename = "type")]
52 pub ty: String,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57#[non_exhaustive]
58pub struct ApiKey {
59 pub id: String,
61 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
63 pub ty: Option<String>,
64 pub name: String,
66 pub partial_key_hint: String,
68 pub status: ApiKeyStatus,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub workspace_id: Option<String>,
74 pub created_by: ApiKeyCreator,
76 pub created_at: String,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub expires_at: Option<String>,
81}
82
83#[derive(Debug, Clone, Default, Serialize)]
86#[non_exhaustive]
87pub struct UpdateApiKeyRequest {
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub name: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub status: Option<WriteApiKeyStatus>,
94}
95
96impl UpdateApiKeyRequest {
97 #[must_use]
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 #[must_use]
105 pub fn name(mut self, name: impl Into<String>) -> Self {
106 self.name = Some(name.into());
107 self
108 }
109
110 #[must_use]
112 pub fn status(mut self, status: WriteApiKeyStatus) -> Self {
113 self.status = Some(status);
114 self
115 }
116}
117
118#[derive(Debug, Clone, Default)]
120#[non_exhaustive]
121pub struct ListApiKeysParams {
122 pub paging: ListParams,
124 pub created_by_user_id: Option<String>,
126 pub status: Option<ApiKeyStatus>,
128 pub workspace_id: Option<String>,
130}
131
132impl ListApiKeysParams {
133 fn to_query(&self) -> Vec<(&'static str, String)> {
134 let mut q = self.paging.to_query();
135 if let Some(u) = &self.created_by_user_id {
136 q.push(("created_by_user_id", u.clone()));
137 }
138 if let Some(s) = self.status {
139 q.push((
140 "status",
141 match s {
142 ApiKeyStatus::Active => "active".into(),
143 ApiKeyStatus::Inactive => "inactive".into(),
144 ApiKeyStatus::Archived => "archived".into(),
145 ApiKeyStatus::Expired => "expired".into(),
146 },
147 ));
148 }
149 if let Some(w) = &self.workspace_id {
150 q.push(("workspace_id", w.clone()));
151 }
152 q
153 }
154}
155
156pub struct ApiKeys<'a> {
158 client: &'a Client,
159}
160
161impl<'a> ApiKeys<'a> {
162 pub(crate) fn new(client: &'a Client) -> Self {
163 Self { client }
164 }
165
166 pub async fn retrieve(&self, key_id: &str) -> Result<ApiKey> {
168 let path = format!("/v1/organizations/api_keys/{key_id}");
169 self.client
170 .execute_with_retry(
171 || self.client.request_builder(reqwest::Method::GET, &path),
172 &[],
173 )
174 .await
175 }
176
177 pub async fn list(&self, params: ListApiKeysParams) -> Result<Paginated<ApiKey>> {
179 let query = params.to_query();
180 self.client
181 .execute_with_retry(
182 || {
183 let mut req = self
184 .client
185 .request_builder(reqwest::Method::GET, "/v1/organizations/api_keys");
186 for (k, v) in &query {
187 req = req.query(&[(k, v)]);
188 }
189 req
190 },
191 &[],
192 )
193 .await
194 }
195
196 pub async fn update(&self, key_id: &str, request: UpdateApiKeyRequest) -> Result<ApiKey> {
198 let path = format!("/v1/organizations/api_keys/{key_id}");
199 let body = &request;
200 self.client
201 .execute_with_retry(
202 || {
203 self.client
204 .request_builder(reqwest::Method::POST, &path)
205 .json(body)
206 },
207 &[],
208 )
209 .await
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use pretty_assertions::assert_eq;
217 use serde_json::json;
218 use wiremock::matchers::{body_partial_json, method, path};
219 use wiremock::{Mock, MockServer, ResponseTemplate};
220
221 fn client_for(mock: &MockServer) -> Client {
222 Client::builder()
223 .api_key("sk-ant-admin-test")
224 .base_url(mock.uri())
225 .build()
226 .unwrap()
227 }
228
229 fn fake_api_key() -> serde_json::Value {
230 json!({
231 "id": "apikey_01",
232 "type": "api_key",
233 "name": "ci",
234 "partial_key_hint": "sk-ant-...abcd",
235 "status": "active",
236 "workspace_id": null,
237 "created_by": {"id": "user_01", "type": "user"},
238 "created_at": "2026-05-01T00:00:00Z",
239 "expires_at": null
240 })
241 }
242
243 #[tokio::test]
244 async fn retrieve_api_key_returns_typed_record() {
245 let mock = MockServer::start().await;
246 Mock::given(method("GET"))
247 .and(path("/v1/organizations/api_keys/apikey_01"))
248 .respond_with(ResponseTemplate::new(200).set_body_json(fake_api_key()))
249 .mount(&mock)
250 .await;
251 let client = client_for(&mock);
252 let k = client
253 .admin()
254 .api_keys()
255 .retrieve("apikey_01")
256 .await
257 .unwrap();
258 assert_eq!(k.id, "apikey_01");
259 assert_eq!(k.status, ApiKeyStatus::Active);
260 assert_eq!(k.created_by.ty, "user");
261 }
262
263 #[tokio::test]
264 async fn list_api_keys_filters_by_status_and_workspace() {
265 let mock = MockServer::start().await;
266 Mock::given(method("GET"))
267 .and(path("/v1/organizations/api_keys"))
268 .and(wiremock::matchers::query_param("status", "active"))
269 .and(wiremock::matchers::query_param("workspace_id", "ws_01"))
270 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
271 "data": [fake_api_key()],
272 "has_more": false,
273 "first_id": "apikey_01",
274 "last_id": "apikey_01"
275 })))
276 .mount(&mock)
277 .await;
278 let client = client_for(&mock);
279 let page = client
280 .admin()
281 .api_keys()
282 .list(ListApiKeysParams {
283 status: Some(ApiKeyStatus::Active),
284 workspace_id: Some("ws_01".into()),
285 ..Default::default()
286 })
287 .await
288 .unwrap();
289 assert_eq!(page.data.len(), 1);
290 }
291
292 #[tokio::test]
293 async fn update_api_key_can_change_name_and_status() {
294 let mock = MockServer::start().await;
295 Mock::given(method("POST"))
296 .and(path("/v1/organizations/api_keys/apikey_01"))
297 .and(body_partial_json(json!({
298 "name": "renamed",
299 "status": "archived"
300 })))
301 .respond_with(ResponseTemplate::new(200).set_body_json(fake_api_key()))
302 .mount(&mock)
303 .await;
304 let client = client_for(&mock);
305 client
306 .admin()
307 .api_keys()
308 .update(
309 "apikey_01",
310 UpdateApiKeyRequest::new()
311 .name("renamed")
312 .status(WriteApiKeyStatus::Archived),
313 )
314 .await
315 .unwrap();
316 }
317}