Skip to main content

openrouter_rs/api/
auth.rs

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/// Request payload for `POST /auth/keys/code`.
45#[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/// Response payload for `POST /auth/keys/code`.
87#[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
95/// Exchange an authorization code from the PKCE flow for a user-controlled API key
96///
97/// # Arguments
98///
99/// * `base_url` - The base URL of the OpenRouter API.
100/// * `code` - The authorization code received from the OAuth redirect.
101/// * `code_verifier` - The code verifier if code_challenge was used in the authorization request.
102/// * `code_challenge_method` - The method used to generate the code challenge.
103///
104/// # Returns
105///
106/// * `Result<AuthResponse, OpenRouterError>` - The API key and user ID associated with the API key.
107pub 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
153/// Create an authorization code for PKCE flow (`POST /auth/keys/code`).
154///
155/// Returns an auth code ID that can be exchanged via [`exchange_code_for_api_key`].
156pub 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}