use std::path::{Path, PathBuf}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use serde_json::json; use sqlx::{Connection, Row, SqliteConnection}; use crate::{Error, Result}; const EMAIL: &str = "cursor@ai.com"; const SIGN_UP_TYPE: &str = "Google"; const SUBJECT: &str = "cursor-local-user"; const MEMBERSHIP_TYPE: &str = "ultra"; const SUBSCRIPTION_STATUS: &str = "active"; pub async fn inject_if_missing() -> Result<()> { inject_if_missing_at(&state_db_path()?).await } fn state_db_path() -> Result { let home = dirs::home_dir() .ok_or_else(|| Error::Config("cannot resolve user home directory".into()))?; match std::env::consts::OS { "macos" => { Ok(home.join("Library/Application Support/Cursor/User/globalStorage/state.vscdb")) } "windows" => Ok(std::env::var_os("APPDATA") .map(PathBuf::from) .unwrap_or_else(|| home.join("AppData/Roaming")) .join("Cursor/User/globalStorage/state.vscdb")), "linux" => Ok(std::env::var_os("XDG_CONFIG_HOME") .map(PathBuf::from) .unwrap_or_else(|| home.join(".config")) .join("Cursor/User/globalStorage/state.vscdb")), platform => Err(Error::Config(format!( "Cursor account injection is unsupported on {platform}" ))), } } async fn inject_if_missing_at(path: &Path) -> Result<()> { if let Some(parent) = path.parent() { tokio::fs::create_dir_all(parent).await?; } let options = sqlx::sqlite::SqliteConnectOptions::new() .filename(path) .create_if_missing(true); let mut connection = SqliteConnection::connect_with(&options).await?; sqlx::query( "CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)", ) .execute(&mut connection) .await?; let account = sqlx::query("SELECT CAST(value AS TEXT) AS value FROM ItemTable WHERE key = ?") .bind("cursorAuth/accessToken") .fetch_optional(&mut connection) .await?; if account.is_some_and(|row| { row.try_get::("value") .is_ok_and(|value| !value.trim().is_empty()) }) { return Ok(()); } let token = local_token()?; let values = [ ("cursorAuth/accessToken", token.as_str()), ("cursorAuth/refreshToken", token.as_str()), ("cursorAuth/cachedEmail", EMAIL), ("cursorAuth/cachedSignUpType", SIGN_UP_TYPE), ("cursorAuth/stripeMembershipType", MEMBERSHIP_TYPE), ("cursorAuth/stripeSubscriptionStatus", SUBSCRIPTION_STATUS), ]; let mut transaction = connection.begin().await?; for (key, value) in values { sqlx::query("INSERT OR REPLACE INTO ItemTable(key, value) VALUES(?, ?)") .bind(key) .bind(value) .execute(&mut *transaction) .await?; } transaction.commit().await?; tracing::info!( email = EMAIL, subject = SUBJECT, "injected local Cursor account" ); Ok(()) } fn local_token() -> Result { let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"HS256","typ":"JWT"}"#); let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&json!({ "sub": SUBJECT, "email": EMAIL, "type": "session", "iss": "cursor-client", "scope": "openid profile email", "exp": 4070908800_u64 }))?); Ok(format!("{header}.{payload}.{SUBJECT}")) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn injects_the_local_account_only_when_missing() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("state.vscdb"); inject_if_missing_at(&path).await.unwrap(); let options = sqlx::sqlite::SqliteConnectOptions::new().filename(&path); let mut connection = SqliteConnection::connect_with(&options).await.unwrap(); let values = sqlx::query("SELECT key, CAST(value AS TEXT) AS value FROM ItemTable") .fetch_all(&mut connection) .await .unwrap() .into_iter() .map(|row| (row.get::("key"), row.get::("value"))) .collect::>(); let token = &values["cursorAuth/accessToken"]; assert_eq!(values["cursorAuth/refreshToken"], *token); assert_eq!(values["cursorAuth/cachedEmail"], EMAIL); assert_eq!(values["cursorAuth/cachedSignUpType"], SIGN_UP_TYPE); assert_eq!(values["cursorAuth/stripeMembershipType"], MEMBERSHIP_TYPE); assert_eq!( values["cursorAuth/stripeSubscriptionStatus"], SUBSCRIPTION_STATUS ); let payload = token.split('.').nth(1).unwrap(); let payload: serde_json::Value = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(payload).unwrap()).unwrap(); assert_eq!(payload["sub"], SUBJECT); assert_eq!(payload["email"], EMAIL); assert_eq!(payload["exp"], 4070908800_u64); sqlx::query("UPDATE ItemTable SET value = 'existing-token' WHERE key = ?") .bind("cursorAuth/accessToken") .execute(&mut connection) .await .unwrap(); drop(connection); inject_if_missing_at(&path).await.unwrap(); let options = sqlx::sqlite::SqliteConnectOptions::new().filename(&path); let mut connection = SqliteConnection::connect_with(&options).await.unwrap(); let token: String = sqlx::query_scalar("SELECT CAST(value AS TEXT) FROM ItemTable WHERE key = ?") .bind("cursorAuth/accessToken") .fetch_one(&mut connection) .await .unwrap(); assert_eq!(token, "existing-token"); } }