Skip to content
This repository was archived by the owner on Feb 18, 2025. It is now read-only.

Commit 0e52ce7

Browse files
ktiaysunixzii
authored andcommitted
Migrate login logic to Rust implementation
1 parent a4378ac commit 0e52ce7

18 files changed

Lines changed: 368 additions & 122 deletions

File tree

.vscode/settings.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
},
1111
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
1212
"typescript.tsc.autoDetect": "off",
13-
"cSpell.words": [
14-
"aicursor"
15-
],
13+
"cSpell.words": ["aicursor", "openai"],
1614
"cmake.configureOnOpen": false,
1715
"editor.formatOnSave": true
1816
}

crates/cursor-core/src/auth/mod.rs

Lines changed: 140 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
1-
pub mod session;
1+
// pub mod session;
22
pub mod token;
33

4-
use node_bridge::bindings::AbortSignal;
4+
use std::future::IntoFuture;
5+
6+
use base64::Engine;
7+
use futures::{
8+
future::{select, Either},
9+
StreamExt,
10+
};
11+
use gloo::timers::future::IntervalStream;
12+
use node_bridge::{bindings::AbortSignal, futures::Defer, http_client::HttpMethod, prelude::*};
513
use rand::RngCore;
14+
use sha2::Digest;
15+
use uuid::Uuid;
616
use wasm_bindgen::prelude::*;
17+
use wasm_bindgen_futures::future_to_promise;
18+
19+
use crate::{
20+
bindings::{progress_location::ProgressLocation, progress_options::ProgressOptions},
21+
context::get_extension_context,
22+
request::make_request,
23+
};
724

8-
use self::session::Session;
25+
use self::token::Token;
926

1027
fn random_bytes() -> Vec<u8> {
1128
let mut rng = rand::thread_rng();
@@ -14,11 +31,128 @@ fn random_bytes() -> Vec<u8> {
1431
bytes
1532
}
1633

17-
#[wasm_bindgen]
18-
pub fn make_session(abort_signal: AbortSignal) -> Session {
19-
Session::new(abort_signal)
34+
fn base64_encode<T>(bytes: T) -> String
35+
where
36+
T: AsRef<[u8]>,
37+
{
38+
base64::engine::general_purpose::STANDARD
39+
.encode(bytes)
40+
.replace("+", "-")
41+
.replace("/", "_")
42+
.replace("=", "")
43+
}
44+
45+
fn sha256<T>(data: T) -> Vec<u8>
46+
where
47+
T: AsRef<[u8]>,
48+
{
49+
let mut hasher = sha2::Sha256::new();
50+
hasher.update(data);
51+
hasher.finalize().to_vec()
2052
}
2153

54+
#[wasm_bindgen(js_name = signIn)]
55+
pub async fn sign_in() {
56+
let uuid = Uuid::new_v4().to_string();
57+
let verifier = base64_encode(random_bytes());
58+
let challenge = base64_encode(sha256(verifier.clone()));
59+
60+
let login_url = format!(
61+
"https://cursor.so/loginDeepControl?challenge={challenge}&uuid={}",
62+
uuid.clone()
63+
);
64+
65+
let context = get_extension_context();
66+
// The API of VSCode does not allow us to obtain the execution result of the 'vscode.open' command,
67+
// so we cannot determine whether the user has confirmed to open url.
68+
context
69+
.execute_command1("vscode.open", JsValue::from_str(&login_url))
70+
.await;
71+
72+
context
73+
.with_progress(
74+
ProgressOptions {
75+
location: ProgressLocation::Notification,
76+
title: Some("Signing in...".to_owned()),
77+
cancellable: true,
78+
},
79+
closure!(|abort_signal: AbortSignal| {
80+
let uuid = uuid.clone();
81+
let verifier = verifier.clone();
82+
future_to_promise(async move {
83+
polling(&uuid, &verifier, abort_signal)
84+
.await
85+
.map(Into::into)
86+
})
87+
})
88+
.into_js_value()
89+
.into(),
90+
)
91+
.await;
92+
}
93+
94+
async fn polling(
95+
uuid: &str,
96+
verifier: &str,
97+
abort_signal: AbortSignal,
98+
) -> Result<Option<String>, JsValue> {
99+
let defer_abort = Defer::new();
100+
let defer_abort_clone = defer_abort.clone();
101+
abort_signal.add_event_listener(
102+
"abort",
103+
closure_once!(|| {
104+
defer_abort_clone.resolve(JsValue::null());
105+
})
106+
.into_js_value(),
107+
);
108+
109+
let mut interval = IntervalStream::new(2000);
110+
loop {
111+
let defer_abort_future = defer_abort.clone().into_future();
112+
match select(defer_abort_future, interval.next()).await {
113+
Either::Left(_) => {
114+
return Ok(None);
115+
}
116+
_ => {}
117+
}
118+
let mut response = make_request(
119+
&format!("/auth/poll?uuid={}&verifier={}", uuid, verifier),
120+
HttpMethod::Get,
121+
)
122+
.send()
123+
.await?;
124+
125+
if let Some(chunk) = response.body().next().await {
126+
let data = chunk.to_string("utf-8");
127+
#[cfg(debug_assertions)]
128+
console::log_str(&data);
129+
match serde_json::from_str::<serde_json::Value>(&data).and_then(|value| {
130+
if value.is_null() {
131+
Ok(false)
132+
} else {
133+
serde_json::from_str::<Token>(&data).map(|_| true)
134+
}
135+
}) {
136+
Ok(flag) => {
137+
if !flag {
138+
continue;
139+
}
140+
return Ok(Some(data));
141+
}
142+
Err(err) => {
143+
let js_error = JsError::new(&err.to_string());
144+
let error = js_error.into();
145+
#[cfg(debug_assertions)]
146+
console::error1(&error);
147+
return Err(error);
148+
}
149+
}
150+
}
151+
}
152+
}
153+
154+
pub fn sign_out() {}
155+
22156
#[cfg(test)]
23157
mod test {
24158
#[test]

crates/cursor-core/src/auth/session.rs

Lines changed: 11 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,34 @@
11
use std::future::IntoFuture;
22

3-
use base64::Engine;
4-
use futures::{
5-
future::{select, Either},
6-
StreamExt,
7-
};
8-
use gloo::timers::future::IntervalStream;
9-
use js_sys::Promise;
10-
use node_bridge::{
11-
bindings::AbortSignal, closure_once, futures::Defer, http_client::HttpMethod, prelude::console,
12-
};
13-
use sha2::Digest;
14-
use uuid::Uuid;
15-
use wasm_bindgen::prelude::*;
16-
use wasm_bindgen_futures::spawn_local;
17-
183
use crate::{auth::random_bytes, request::make_request};
194

205
use super::token::Token;
216

22-
fn base64_encode<T>(bytes: T) -> String
23-
where
24-
T: AsRef<[u8]>,
25-
{
26-
base64::engine::general_purpose::STANDARD
27-
.encode(bytes)
28-
.replace("+", "-")
29-
.replace("/", "_")
30-
.replace("=", "")
31-
}
32-
33-
fn sha256<T>(data: T) -> Vec<u8>
34-
where
35-
T: AsRef<[u8]>,
36-
{
37-
let mut hasher = sha2::Sha256::new();
38-
hasher.update(data);
39-
hasher.finalize().to_vec()
40-
}
41-
42-
#[wasm_bindgen(getter_with_clone)]
7+
#[wasm_bindgen(getter_with_clone, js_name = AuthSession)]
438
pub struct Session {
9+
#[wasm_bindgen(js_name = loginUrl)]
4410
pub login_url: String,
45-
pub token: Promise,
11+
12+
uuid: String,
13+
verifier: String,
4614
}
4715

16+
#[wasm_bindgen(js_class = AuthSession)]
4817
impl Session {
49-
pub fn new(abort_signal: AbortSignal) -> Self {
50-
let uuid = Uuid::new_v4().to_string();
51-
let verifier = base64_encode(random_bytes());
52-
let challenge = base64_encode(sha256(verifier.clone()));
53-
let defer_abort = Defer::new();
54-
let defer_abort_clone = defer_abort.clone();
55-
abort_signal.add_event_listener(
56-
"abort",
57-
closure_once!(|| {
58-
defer_abort_clone.resolve(JsValue::null());
59-
})
60-
.into_js_value(),
61-
);
18+
#[wasm_bindgen(constructor)]
19+
pub fn new() -> Self {
6220
Self {
6321
login_url: format!(
6422
"https://cursor.so/loginDeepControl?challenge={challenge}&uuid={}",
6523
uuid.clone()
6624
)
6725
.to_owned(),
68-
token: Promise::new(&mut |resolve, reject| {
69-
let uuid = uuid.clone();
70-
let verifier = verifier.clone();
71-
let defer_abort = defer_abort.clone();
72-
spawn_local(async move {
73-
let mut interval = IntervalStream::new(2000);
74-
loop {
75-
let defer_abort_future = defer_abort.clone().into_future();
76-
match select(defer_abort_future, interval.next()).await {
77-
Either::Left(_) => {
78-
let _ = resolve.call1(&JsValue::null(), &JsValue::null());
79-
return;
80-
}
81-
_ => {}
82-
}
83-
if let Ok(mut response) = make_request(
84-
&format!("/auth/poll?uuid={uuid}&verifier={verifier}"),
85-
HttpMethod::Get,
86-
)
87-
.send()
88-
.await
89-
{
90-
if let Some(chunk) = response.body().next().await {
91-
let data = chunk.to_string("utf-8");
92-
#[cfg(debug_assertions)]
93-
console::log_str(&data);
94-
match serde_json::from_str::<serde_json::Value>(&data).and_then(
95-
|value| {
96-
if value.is_null() {
97-
Ok(false)
98-
} else {
99-
serde_json::from_str::<Token>(&data).map(|_| true)
100-
}
101-
},
102-
) {
103-
Ok(flag) => {
104-
if !flag {
105-
continue;
106-
}
107-
let _ = resolve
108-
.call1(&JsValue::null(), &JsValue::from_str(&data));
109-
}
110-
Err(err) => {
111-
let js_error = JsError::new(&err.to_string());
112-
let error = js_error.into();
113-
#[cfg(debug_assertions)]
114-
console::error1(&error);
115-
let _ = reject.call1(&JsValue::null(), &error);
116-
}
117-
}
118-
return;
119-
}
120-
let _ = response.await;
121-
}
122-
}
123-
})
124-
}),
26+
uuid,
27+
verifier,
12528
}
12629
}
12730

128-
pub fn cancel_polling(&self) {}
31+
pub async fn polling(&self, abort_signal: AbortSignal) -> Result<Option<String>, JsValue> {}
12932
}
13033

13134
#[cfg(test)]

crates/cursor-core/src/auth/token.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use serde::{Deserialize, Serialize};
2-
use wasm_bindgen::prelude::*;
32

4-
#[wasm_bindgen(getter_with_clone)]
53
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64
#[serde(rename_all = "camelCase")]
75
pub struct Token {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use wasm_bindgen::prelude::*;
2+
3+
#[wasm_bindgen]
4+
extern "C" {
5+
pub type CancellationToken;
6+
7+
#[wasm_bindgen(method, getter, structural, js_name = isCancellationRequested)]
8+
pub fn is_cancellation_requested(this: &CancellationToken) -> bool;
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod cancellation_token;
2+
pub mod progress_location;
3+
pub mod progress_options;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use wasm_bindgen::prelude::*;
2+
3+
#[wasm_bindgen(js_name = RustProgressLocation)]
4+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5+
pub enum ProgressLocation {
6+
SourceControl = 1,
7+
8+
/**
9+
* Show progress in the status bar of the editor. Neither supports cancellation nor discrete progress.
10+
* Supports rendering of {@link ThemeIcon theme icons} via the `$(<name>)`-syntax in the progress label.
11+
*/
12+
Window = 10,
13+
14+
/**
15+
* Show progress as notification with an optional cancel button. Supports to show infinite and discrete
16+
* progress but does not support rendering of icons.
17+
*/
18+
Notification = 15,
19+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use super::progress_location::ProgressLocation;
2+
use wasm_bindgen::prelude::*;
3+
4+
#[wasm_bindgen(getter_with_clone, js_name = RustProgressOptions)]
5+
pub struct ProgressOptions {
6+
pub location: ProgressLocation,
7+
pub title: Option<String>,
8+
pub cancellable: bool,
9+
}

0 commit comments

Comments
 (0)