1use derive_builder::Builder;
2use reqwest::Client as HttpClient;
3use serde::{Deserialize, Serialize};
4
5use crate::{
6 error::OpenRouterError,
7 transport::{request as transport_request, response as transport_response},
8 types::ApiResponse,
9};
10
11#[derive(Serialize, Deserialize, Debug)]
12#[non_exhaustive]
13pub struct AuthRequest {
14 code: String,
15 code_verifier: Option<String>,
16 code_challenge_method: Option<CodeChallengeMethod>,
17}
18
19#[derive(Serialize, Deserialize, Debug, Clone)]
20#[non_exhaustive]
21#[serde(rename_all = "lowercase")]
22pub enum CodeChallengeMethod {
23 #[serde(rename = "S256")]
24 S256,
25 Plain,
26}
27
28#[derive(Serialize, Deserialize, Debug)]
29#[non_exhaustive]
30pub struct AuthResponse {
31 pub key: String,
32 pub user_id: Option<String>,
33}
34
35#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
36#[non_exhaustive]
37#[serde(rename_all = "lowercase")]
38pub enum UsageLimitType {
39 Daily,
40 Weekly,
41 Monthly,
42}
43
44#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
46#[builder(build_fn(error = "OpenRouterError"))]
47#[non_exhaustive]
48pub struct CreateAuthCodeRequest {
49 #[builder(setter(into))]
50 callback_url: String,
51 #[builder(setter(into, strip_option), default)]
52 #[serde(skip_serializing_if = "Option::is_none")]
53 code_challenge: Option<String>,
54 #[builder(setter(strip_option), default)]
55 #[serde(skip_serializing_if = "Option::is_none")]
56 code_challenge_method: Option<CodeChallengeMethod>,
57 #[builder(setter(strip_option), default)]
58 #[serde(skip_serializing_if = "Option::is_none")]
59 limit: Option<f64>,
60 #[builder(setter(into, strip_option), default)]
61 #[serde(skip_serializing_if = "Option::is_none")]
62 expires_at: Option<String>,
63 #[builder(setter(into, strip_option), default)]
64 #[serde(skip_serializing_if = "Option::is_none")]
65 key_label: Option<String>,
66 #[builder(setter(strip_option), default)]
67 #[serde(skip_serializing_if = "Option::is_none")]
68 usage_limit_type: Option<UsageLimitType>,
69 #[builder(setter(into, strip_option), default)]
70 #[serde(skip_serializing_if = "Option::is_none")]
71 spawn_agent: Option<String>,
72 #[builder(setter(into, strip_option), default)]
73 #[serde(skip_serializing_if = "Option::is_none")]
74 spawn_cloud: Option<String>,
75 #[builder(setter(into, strip_option), default)]
76 #[serde(skip_serializing_if = "Option::is_none")]
77 workspace_id: Option<String>,
78}
79
80impl CreateAuthCodeRequest {
81 pub fn builder() -> CreateAuthCodeRequestBuilder {
82 CreateAuthCodeRequestBuilder::default()
83 }
84}
85
86#[derive(Serialize, Deserialize, Debug, Clone)]
88#[non_exhaustive]
89pub struct AuthCodeData {
90 pub id: String,
91 pub app_id: f64,
92 pub created_at: String,
93}
94
95pub async fn exchange_code_for_api_key(
108 base_url: &str,
109 code: &str,
110 code_verifier: Option<&str>,
111 code_challenge_method: Option<CodeChallengeMethod>,
112) -> Result<AuthResponse, OpenRouterError> {
113 let http_client = crate::transport::new_client()?;
114 exchange_code_for_api_key_with_client(
115 &http_client,
116 base_url,
117 code,
118 code_verifier,
119 code_challenge_method,
120 )
121 .await
122}
123
124pub(crate) async fn exchange_code_for_api_key_with_client(
125 http_client: &HttpClient,
126 base_url: &str,
127 code: &str,
128 code_verifier: Option<&str>,
129 code_challenge_method: Option<CodeChallengeMethod>,
130) -> Result<AuthResponse, OpenRouterError> {
131 let url = format!("{base_url}/auth/keys");
132 let request = AuthRequest {
133 code: code.to_string(),
134 code_verifier: code_verifier.map(|s| s.to_string()),
135 code_challenge_method,
136 };
137
138 let response = transport_request::post(http_client, &url)
139 .json(&request)
140 .send()
141 .await?;
142
143 if response.status().is_success() {
144 let auth_response: AuthResponse =
145 transport_response::parse_json_response(response, "auth key exchange").await?;
146 Ok(auth_response)
147 } else {
148 transport_response::handle_error(response).await?;
149 unreachable!()
150 }
151}
152
153pub async fn create_auth_code(
157 base_url: &str,
158 api_key: &str,
159 request: &CreateAuthCodeRequest,
160) -> Result<AuthCodeData, OpenRouterError> {
161 let http_client = crate::transport::new_client()?;
162 create_auth_code_with_client(&http_client, base_url, api_key, request).await
163}
164
165pub(crate) async fn create_auth_code_with_client(
166 http_client: &HttpClient,
167 base_url: &str,
168 api_key: &str,
169 request: &CreateAuthCodeRequest,
170) -> Result<AuthCodeData, OpenRouterError> {
171 let url = format!("{base_url}/auth/keys/code");
172 let response =
173 transport_request::with_bearer_auth(transport_request::post(http_client, &url), api_key)
174 .json(request)
175 .send()
176 .await?;
177
178 if response.status().is_success() {
179 let payload: ApiResponse<AuthCodeData> =
180 transport_response::parse_json_response(response, "auth code creation").await?;
181 Ok(payload.data)
182 } else {
183 transport_response::handle_error(response).await?;
184 unreachable!()
185 }
186}