mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-05 20:44:07 +08:00
468 lines
16 KiB
Rust
468 lines
16 KiB
Rust
use axum::{
|
|
body::{Body, Bytes},
|
|
extract::Extension,
|
|
http::{header, Request, Response},
|
|
};
|
|
use prost::Message;
|
|
use serde_json::{Map, Value};
|
|
|
|
use crate::{cursor::proxy, Result};
|
|
|
|
const LOCAL_AUTH_ID: &str = "local_ultra";
|
|
const LOCAL_EMAIL: &str = "cursor@ai.com";
|
|
const LOCAL_ULTRA_PLAN_INCLUDED_CENTS: i32 = 20_000;
|
|
|
|
#[derive(Clone, PartialEq, Message)]
|
|
struct GetEmailResponse {
|
|
#[prost(string, tag = "1")]
|
|
email: String,
|
|
#[prost(int32, tag = "2")]
|
|
sign_up_type: i32,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, Message)]
|
|
struct GetMeResponse {
|
|
#[prost(string, tag = "1")]
|
|
auth_id: String,
|
|
#[prost(int32, tag = "2")]
|
|
user_id: i32,
|
|
#[prost(string, optional, tag = "3")]
|
|
email: Option<String>,
|
|
#[prost(string, optional, tag = "4")]
|
|
first_name: Option<String>,
|
|
#[prost(string, optional, tag = "5")]
|
|
last_name: Option<String>,
|
|
#[prost(string, optional, tag = "8")]
|
|
created_at: Option<String>,
|
|
#[prost(bool, optional, tag = "9")]
|
|
is_enterprise_user: Option<bool>,
|
|
#[prost(string, optional, tag = "11")]
|
|
email_domain_type: Option<String>,
|
|
#[prost(string, optional, tag = "12")]
|
|
country: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, Message)]
|
|
struct GetUserProfileResponse {
|
|
#[prost(bool, optional, tag = "4")]
|
|
public_visibility_allowed: Option<bool>,
|
|
#[prost(string, optional, tag = "5")]
|
|
max_visibility: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, Message)]
|
|
struct GetCurrentPeriodUsageResponse {
|
|
#[prost(int64, tag = "1")]
|
|
billing_cycle_start: i64,
|
|
#[prost(int64, tag = "2")]
|
|
billing_cycle_end: i64,
|
|
#[prost(message, optional, tag = "3")]
|
|
plan_usage: Option<PlanUsage>,
|
|
#[prost(message, optional, tag = "4")]
|
|
spend_limit_usage: Option<SpendLimitUsage>,
|
|
#[prost(int32, optional, tag = "5")]
|
|
display_threshold: Option<i32>,
|
|
#[prost(bool, tag = "6")]
|
|
enabled: bool,
|
|
#[prost(string, tag = "7")]
|
|
display_message: String,
|
|
#[prost(string, optional, tag = "11")]
|
|
auto_model_selected_display_message: Option<String>,
|
|
#[prost(string, optional, tag = "12")]
|
|
named_model_selected_display_message: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, Message)]
|
|
struct PlanUsage {
|
|
#[prost(int32, tag = "1")]
|
|
total_spend: i32,
|
|
#[prost(int32, tag = "2")]
|
|
included_spend: i32,
|
|
#[prost(int32, tag = "4")]
|
|
remaining: i32,
|
|
#[prost(int32, tag = "5")]
|
|
limit: i32,
|
|
#[prost(bool, optional, tag = "6")]
|
|
remaining_bonus: Option<bool>,
|
|
#[prost(string, optional, tag = "7")]
|
|
bonus_tooltip: Option<String>,
|
|
#[prost(int32, optional, tag = "8")]
|
|
auto_spend: Option<i32>,
|
|
#[prost(int32, optional, tag = "9")]
|
|
api_spend: Option<i32>,
|
|
#[prost(double, optional, tag = "12")]
|
|
auto_percent_used: Option<f64>,
|
|
#[prost(double, optional, tag = "13")]
|
|
api_percent_used: Option<f64>,
|
|
#[prost(double, optional, tag = "14")]
|
|
total_percent_used: Option<f64>,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, Message)]
|
|
struct SpendLimitUsage {
|
|
#[prost(string, tag = "8")]
|
|
limit_type: String,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, Message)]
|
|
struct GetUsageLimitStatusAndActiveGrantsResponse {
|
|
#[prost(message, optional, tag = "1")]
|
|
usage_limit_policy_status: Option<UsageLimitPolicyStatus>,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq, Message)]
|
|
struct UsageLimitPolicyStatus {
|
|
#[prost(bool, tag = "1")]
|
|
is_in_slow_pool: bool,
|
|
#[prost(map = "string, string", tag = "5")]
|
|
features: std::collections::HashMap<String, String>,
|
|
#[prost(bool, tag = "6")]
|
|
can_configure_spend_limit: bool,
|
|
#[prost(bool, tag = "8")]
|
|
has_pending_request: bool,
|
|
#[prost(string, repeated, tag = "9")]
|
|
allowed_model_ids: Vec<String>,
|
|
#[prost(string, repeated, tag = "10")]
|
|
allowed_model_tags: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Message)]
|
|
struct Empty {}
|
|
|
|
pub async fn get_email(
|
|
Extension(upstream): Extension<proxy::CursorProxy>,
|
|
request: Request<Body>,
|
|
) -> Result<Response<Body>> {
|
|
forward_or(upstream, request, || {
|
|
proto(GetEmailResponse {
|
|
email: LOCAL_EMAIL.into(),
|
|
sign_up_type: 3,
|
|
})
|
|
})
|
|
.await
|
|
}
|
|
|
|
pub async fn get_me(
|
|
Extension(upstream): Extension<proxy::CursorProxy>,
|
|
request: Request<Body>,
|
|
) -> Result<Response<Body>> {
|
|
forward_or(upstream, request, || {
|
|
proto(GetMeResponse {
|
|
auth_id: LOCAL_AUTH_ID.into(),
|
|
user_id: 1,
|
|
email: Some(LOCAL_EMAIL.into()),
|
|
first_name: Some("Cursor".into()),
|
|
last_name: Some("Local".into()),
|
|
created_at: Some(chrono::Utc::now().to_rfc3339()),
|
|
is_enterprise_user: Some(false),
|
|
email_domain_type: Some("personal".into()),
|
|
country: Some("US".into()),
|
|
})
|
|
})
|
|
.await
|
|
}
|
|
|
|
pub async fn get_teams(
|
|
Extension(upstream): Extension<proxy::CursorProxy>,
|
|
request: Request<Body>,
|
|
) -> Result<Response<Body>> {
|
|
forward_or(upstream, request, || proto(Empty {})).await
|
|
}
|
|
|
|
pub async fn get_user_profile(
|
|
Extension(upstream): Extension<proxy::CursorProxy>,
|
|
request: Request<Body>,
|
|
) -> Result<Response<Body>> {
|
|
forward_or(upstream, request, || {
|
|
proto(GetUserProfileResponse {
|
|
public_visibility_allowed: Some(true),
|
|
max_visibility: Some("PUBLIC".into()),
|
|
})
|
|
})
|
|
.await
|
|
}
|
|
|
|
pub async fn current_period_usage() -> Result<Response<Body>> {
|
|
let now = chrono::Utc::now();
|
|
proto(GetCurrentPeriodUsageResponse {
|
|
billing_cycle_start: (now - chrono::Duration::days(30)).timestamp_millis(),
|
|
billing_cycle_end: (now + chrono::Duration::days(10 * 365)).timestamp_millis(),
|
|
plan_usage: Some(PlanUsage {
|
|
total_spend: 0,
|
|
included_spend: LOCAL_ULTRA_PLAN_INCLUDED_CENTS,
|
|
remaining: LOCAL_ULTRA_PLAN_INCLUDED_CENTS,
|
|
limit: LOCAL_ULTRA_PLAN_INCLUDED_CENTS,
|
|
remaining_bonus: Some(false),
|
|
bonus_tooltip: Some("Ultra local account mock is active.".into()),
|
|
auto_spend: Some(0),
|
|
api_spend: Some(0),
|
|
auto_percent_used: Some(0.0),
|
|
api_percent_used: Some(0.0),
|
|
total_percent_used: Some(0.0),
|
|
}),
|
|
spend_limit_usage: Some(SpendLimitUsage {
|
|
limit_type: "user".into(),
|
|
}),
|
|
display_threshold: Some(99_999_999),
|
|
enabled: true,
|
|
display_message: "Ultra plan active".into(),
|
|
auto_model_selected_display_message: Some("Ultra plan active".into()),
|
|
named_model_selected_display_message: Some("Ultra plan active".into()),
|
|
})
|
|
}
|
|
|
|
pub async fn usage_limit_status() -> Result<Response<Body>> {
|
|
proto(GetUsageLimitStatusAndActiveGrantsResponse {
|
|
usage_limit_policy_status: Some(UsageLimitPolicyStatus {
|
|
is_in_slow_pool: false,
|
|
features: Default::default(),
|
|
can_configure_spend_limit: true,
|
|
has_pending_request: false,
|
|
allowed_model_ids: Vec::new(),
|
|
allowed_model_tags: Vec::new(),
|
|
}),
|
|
})
|
|
}
|
|
|
|
pub async fn stripe_profile(
|
|
Extension(upstream): Extension<proxy::CursorProxy>,
|
|
request: Request<Body>,
|
|
) -> Result<Response<Body>> {
|
|
match proxy::forward_buffered(&upstream, request).await {
|
|
Ok(response) if response.status.is_success() => {
|
|
let mut profile = serde_json::from_slice::<Map<String, Value>>(&response.body)?;
|
|
ultra(&mut profile);
|
|
Ok(response.with_body(Bytes::from(serde_json::to_vec(&profile)?)))
|
|
}
|
|
Ok(response) => {
|
|
tracing::warn!(status = %response.status, "Cursor account upstream rejected profile; using local Ultra identity");
|
|
json(ultra_profile())
|
|
}
|
|
Err(error) => {
|
|
tracing::warn!(%error, "Cursor account upstream unavailable; using local Ultra identity");
|
|
json(ultra_profile())
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn forward_or(
|
|
upstream: proxy::CursorProxy,
|
|
request: Request<Body>,
|
|
fallback: impl FnOnce() -> Result<Response<Body>>,
|
|
) -> Result<Response<Body>> {
|
|
match proxy::forward_buffered(&upstream, request).await {
|
|
Ok(response) if response.status.is_success() => Ok(response.into_response()),
|
|
Ok(response) => {
|
|
tracing::warn!(status = %response.status, "Cursor identity upstream rejected request; using local identity");
|
|
fallback()
|
|
}
|
|
Err(error) => {
|
|
tracing::warn!(%error, "Cursor identity upstream unavailable; using local identity");
|
|
fallback()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn proto(message: impl Message) -> Result<Response<Body>> {
|
|
response("application/proto", message.encode_to_vec())
|
|
}
|
|
|
|
fn json(value: Value) -> Result<Response<Body>> {
|
|
response("application/json", serde_json::to_vec(&value)?)
|
|
}
|
|
|
|
fn response(content_type: &'static str, body: Vec<u8>) -> Result<Response<Body>> {
|
|
let length = body.len();
|
|
let mut response = Response::new(Body::from(body));
|
|
response.headers_mut().insert(
|
|
header::CONTENT_TYPE,
|
|
axum::http::HeaderValue::from_static(content_type),
|
|
);
|
|
response.headers_mut().insert(
|
|
header::CONTENT_LENGTH,
|
|
length
|
|
.to_string()
|
|
.parse()
|
|
.expect("body length is always a valid header value"),
|
|
);
|
|
Ok(response)
|
|
}
|
|
|
|
fn ultra(profile: &mut Map<String, Value>) {
|
|
profile.insert("membershipType".into(), Value::String("ultra".into()));
|
|
profile.insert(
|
|
"individualMembershipType".into(),
|
|
Value::String("ultra".into()),
|
|
);
|
|
profile.insert("subscriptionStatus".into(), Value::String("active".into()));
|
|
}
|
|
|
|
fn ultra_profile() -> Value {
|
|
serde_json::json!({
|
|
"membershipType": "ultra",
|
|
"individualMembershipType": "ultra",
|
|
"subscriptionStatus": "active",
|
|
"lastPaymentFailed": false,
|
|
"pendingCancellationDate": null,
|
|
"daysRemainingOnTrial": 0,
|
|
"paymentId": LOCAL_AUTH_ID,
|
|
"isTeamMember": false
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use axum::{
|
|
body::to_bytes,
|
|
http::StatusCode,
|
|
routing::{get, post},
|
|
Extension, Router,
|
|
};
|
|
use tower::ServiceExt;
|
|
|
|
use super::*;
|
|
|
|
async fn app(upstream: Router) -> (Router, tokio::task::JoinHandle<()>) {
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let address = listener.local_addr().unwrap();
|
|
let server = tokio::spawn(async move { axum::serve(listener, upstream).await.unwrap() });
|
|
let proxy = proxy::CursorProxy::for_upstream(&format!("http://{address}")).unwrap();
|
|
let app = Router::new()
|
|
.route("/auth/full_stripe_profile", get(stripe_profile))
|
|
.route("/aiserver.v1.DashboardService/GetMe", post(get_me))
|
|
.route(
|
|
"/aiserver.v1.DashboardService/GetCurrentPeriodUsage",
|
|
post(current_period_usage),
|
|
)
|
|
.route(
|
|
"/aiserver.v1.DashboardService/GetUsageLimitStatusAndActiveGrants",
|
|
post(usage_limit_status),
|
|
)
|
|
.layer(Extension(proxy));
|
|
(app, server)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn preserves_upstream_profile_and_overlays_ultra_membership() {
|
|
let upstream = Router::new().route(
|
|
"/auth/full_stripe_profile",
|
|
get(|| async {
|
|
axum::Json(serde_json::json!({
|
|
"membershipType": "pro",
|
|
"subscriptionStatus": "inactive",
|
|
"paymentId": "upstream-payment"
|
|
}))
|
|
}),
|
|
);
|
|
let (app, server) = app(upstream).await;
|
|
let response = app
|
|
.oneshot(
|
|
Request::get("/auth/full_stripe_profile")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
|
let profile: Value = serde_json::from_slice(&body).unwrap();
|
|
assert_eq!(profile["membershipType"], "ultra");
|
|
assert_eq!(profile["paymentId"], "upstream-payment");
|
|
server.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn upstream_error_uses_local_identity_without_reading_authorization() {
|
|
let upstream = Router::new().route(
|
|
"/aiserver.v1.DashboardService/GetMe",
|
|
post(|| async { StatusCode::UNAUTHORIZED }),
|
|
);
|
|
let (app, server) = app(upstream).await;
|
|
let response = app
|
|
.oneshot(
|
|
Request::post("/aiserver.v1.DashboardService/GetMe")
|
|
.header(header::AUTHORIZATION, "Bearer ignored")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
|
let identity = GetMeResponse::decode(body).unwrap();
|
|
assert_eq!(identity.auth_id, LOCAL_AUTH_ID);
|
|
assert_eq!(identity.email.as_deref(), Some(LOCAL_EMAIL));
|
|
server.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stripe_error_uses_the_complete_local_ultra_profile() {
|
|
let upstream = Router::new().route(
|
|
"/auth/full_stripe_profile",
|
|
get(|| async { StatusCode::SERVICE_UNAVAILABLE }),
|
|
);
|
|
let (app, server) = app(upstream).await;
|
|
let response = app
|
|
.oneshot(
|
|
Request::get("/auth/full_stripe_profile")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
|
let profile: Value = serde_json::from_slice(&body).unwrap();
|
|
assert_eq!(profile["membershipType"], "ultra");
|
|
assert_eq!(profile["paymentId"], LOCAL_AUTH_ID);
|
|
server.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn current_period_usage_is_a_local_unused_ultra_allowance() {
|
|
let (app, server) = app(Router::new()).await;
|
|
let before = chrono::Utc::now().timestamp_millis();
|
|
let response = app
|
|
.oneshot(
|
|
Request::post("/aiserver.v1.DashboardService/GetCurrentPeriodUsage")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
|
let usage = GetCurrentPeriodUsageResponse::decode(body).unwrap();
|
|
let plan = usage.plan_usage.unwrap();
|
|
assert_eq!(plan.total_spend, 0);
|
|
assert_eq!(plan.limit, LOCAL_ULTRA_PLAN_INCLUDED_CENTS);
|
|
assert_eq!(plan.remaining, LOCAL_ULTRA_PLAN_INCLUDED_CENTS);
|
|
assert_eq!(usage.display_message, "Ultra plan active");
|
|
assert!(usage.billing_cycle_start < before);
|
|
assert!(usage.billing_cycle_end > before);
|
|
server.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn usage_limit_status_is_local_and_unrestricted() {
|
|
let (app, server) = app(Router::new()).await;
|
|
let response = app
|
|
.oneshot(
|
|
Request::post("/aiserver.v1.DashboardService/GetUsageLimitStatusAndActiveGrants")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
|
let response = GetUsageLimitStatusAndActiveGrantsResponse::decode(body).unwrap();
|
|
let policy = response.usage_limit_policy_status.unwrap();
|
|
assert!(!policy.is_in_slow_pool);
|
|
assert!(policy.can_configure_spend_limit);
|
|
assert!(!policy.has_pending_request);
|
|
assert!(policy.allowed_model_ids.is_empty());
|
|
assert!(policy.allowed_model_tags.is_empty());
|
|
server.abort();
|
|
}
|
|
}
|