mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-10 05:48:52 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
784a3d3a32 | ||
|
|
077bfa87b8 | ||
|
|
733f2e5929 | ||
|
|
0f994f89ac | ||
|
|
24f76398f9 | ||
|
|
14475915df | ||
|
|
a50e04f565 | ||
|
|
32fbd66912 | ||
|
|
3503a9da8e | ||
|
|
cef6abf5d0 | ||
|
|
3f098f95fe |
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "24"
|
||||||
|
|
||||||
- name: Install source generators
|
- name: Install source generators
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use tokio::net::{TcpListener, TcpStream};
|
|||||||
const OAUTH_CLIENT_ID: &str = "a1fe44800c2d7e803cad1b4bf07a291c";
|
const OAUTH_CLIENT_ID: &str = "a1fe44800c2d7e803cad1b4bf07a291c";
|
||||||
const KEYRING_USER: &str = "yaak";
|
const KEYRING_USER: &str = "yaak";
|
||||||
const AUTH_TIMEOUT: Duration = Duration::from_secs(300);
|
const AUTH_TIMEOUT: Duration = Duration::from_secs(300);
|
||||||
|
const CALLBACK_READ_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
const MAX_REQUEST_BYTES: usize = 16 * 1024;
|
const MAX_REQUEST_BYTES: usize = 16 * 1024;
|
||||||
|
|
||||||
type CommandResult<T = ()> = std::result::Result<T, String>;
|
type CommandResult<T = ()> = std::result::Result<T, String>;
|
||||||
@@ -209,35 +210,71 @@ async fn receive_oauth_code(
|
|||||||
expected_state: &str,
|
expected_state: &str,
|
||||||
app_base_url: &str,
|
app_base_url: &str,
|
||||||
) -> CommandResult<String> {
|
) -> CommandResult<String> {
|
||||||
|
// Browsers speculatively open extra connections that may never carry a
|
||||||
|
// request. Handle each connection concurrently so an idle socket can't
|
||||||
|
// block the one carrying the real callback.
|
||||||
|
let (tx, mut rx) = tokio::sync::mpsc::channel::<CommandResult<String>>(1);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (mut stream, _) = listener
|
tokio::select! {
|
||||||
.accept()
|
accepted = listener.accept() => {
|
||||||
.await
|
let (stream, _) = accepted
|
||||||
.map_err(|e| format!("OAuth callback server accept error: {e}"))?;
|
.map_err(|e| format!("OAuth callback server accept error: {e}"))?;
|
||||||
|
tokio::spawn(handle_callback_connection(
|
||||||
match parse_callback_request(&mut stream).await {
|
stream,
|
||||||
Ok((state, code)) => {
|
expected_state.to_string(),
|
||||||
if state != expected_state {
|
app_base_url.to_string(),
|
||||||
let _ = write_bad_request(&mut stream, "Invalid OAuth state").await;
|
tx.clone(),
|
||||||
continue;
|
));
|
||||||
}
|
|
||||||
|
|
||||||
let success_redirect = format!("{app_base_url}/login/oauth/success");
|
|
||||||
write_redirect(&mut stream, &success_redirect)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed responding to OAuth callback: {e}"))?;
|
|
||||||
return Ok(code);
|
|
||||||
}
|
}
|
||||||
Err(error) => {
|
result = rx.recv() => {
|
||||||
let _ = write_bad_request(&mut stream, &error).await;
|
if let Some(result) = result {
|
||||||
if error.starts_with("OAuth provider returned error:") {
|
return result;
|
||||||
return Err(error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_callback_connection(
|
||||||
|
mut stream: TcpStream,
|
||||||
|
expected_state: String,
|
||||||
|
app_base_url: String,
|
||||||
|
tx: tokio::sync::mpsc::Sender<CommandResult<String>>,
|
||||||
|
) {
|
||||||
|
let parsed = match tokio::time::timeout(
|
||||||
|
CALLBACK_READ_TIMEOUT,
|
||||||
|
parse_callback_request(&mut stream),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(parsed) => parsed,
|
||||||
|
Err(_) => return, // Idle speculative connection; drop it
|
||||||
|
};
|
||||||
|
|
||||||
|
match parsed {
|
||||||
|
Ok((state, code)) => {
|
||||||
|
if state != expected_state {
|
||||||
|
let _ = write_bad_request(&mut stream, "Invalid OAuth state").await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let success_redirect = format!("{app_base_url}/login/oauth/success");
|
||||||
|
let result = match write_redirect(&mut stream, &success_redirect).await {
|
||||||
|
Ok(()) => Ok(code),
|
||||||
|
Err(e) => Err(format!("Failed responding to OAuth callback: {e}")),
|
||||||
|
};
|
||||||
|
let _ = tx.send(result).await;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
let _ = write_bad_request(&mut stream, &error).await;
|
||||||
|
if error.starts_with("OAuth provider returned error:") {
|
||||||
|
let _ = tx.send(Err(error)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn parse_callback_request(stream: &mut TcpStream) -> CommandResult<(String, String)> {
|
async fn parse_callback_request(stream: &mut TcpStream) -> CommandResult<(String, String)> {
|
||||||
let target = read_http_target(stream).await?;
|
let target = read_http_target(stream).await?;
|
||||||
if !target.starts_with("/oauth/callback") {
|
if !target.starts_with("/oauth/callback") {
|
||||||
@@ -488,6 +525,37 @@ mod tests {
|
|||||||
assert!(err.contains("User denied"));
|
assert!(err.contains("User denied"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn receive_oauth_code_ignores_idle_speculative_connections() {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
||||||
|
let addr = listener.local_addr().expect("local addr");
|
||||||
|
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
receive_oauth_code(listener, "expected-state", "http://localhost:9444").await
|
||||||
|
});
|
||||||
|
|
||||||
|
// Browsers preconnect sockets that never carry a request; these must
|
||||||
|
// not block the connection carrying the real callback.
|
||||||
|
let _idle1 = TcpStream::connect(addr).await.expect("connect idle 1");
|
||||||
|
let _idle2 = TcpStream::connect(addr).await.expect("connect idle 2");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
let mut client = TcpStream::connect(addr).await.expect("connect");
|
||||||
|
client
|
||||||
|
.write_all(
|
||||||
|
b"GET /oauth/callback?code=abc123&state=expected-state HTTP/1.1\r\nHost: localhost\r\n\r\n",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("write");
|
||||||
|
|
||||||
|
let code = tokio::time::timeout(std::time::Duration::from_secs(2), server)
|
||||||
|
.await
|
||||||
|
.expect("idle connections must not block the real callback")
|
||||||
|
.expect("join")
|
||||||
|
.expect("should return code");
|
||||||
|
assert_eq!(code, "abc123");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn receive_oauth_code_fails_fast_on_provider_error() {
|
async fn receive_oauth_code_fails_fast_on_provider_error() {
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
||||||
|
|||||||
@@ -1098,7 +1098,7 @@ impl PluginManager {
|
|||||||
&InternalEventPayload::ImportRequest(ImportRequest {
|
&InternalEventPayload::ImportRequest(ImportRequest {
|
||||||
content: content.to_string(),
|
content: content.to_string(),
|
||||||
}),
|
}),
|
||||||
Duration::from_secs(5),
|
Duration::from_secs(60),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
Generated
+79
-37
@@ -83,12 +83,12 @@
|
|||||||
"@tauri-apps/cli": "npm:@tauri-apps/cli-cef@3.0.0-alpha.6",
|
"@tauri-apps/cli": "npm:@tauri-apps/cli-cef@3.0.0-alpha.6",
|
||||||
"@types/babel__core": "^7.20.5",
|
"@types/babel__core": "^7.20.5",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"@yaakapp/cli": "latest",
|
"@yaakapp/cli": "*",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"nodejs-file-downloader": "^4.13.0",
|
"nodejs-file-downloader": "^4.13.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss": "^8.5.16",
|
"postcss": "^8.5.25",
|
||||||
"tailwindcss": "^4.3.2",
|
"tailwindcss": "^4.3.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
|
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
|
||||||
@@ -6331,20 +6331,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "2.2.2",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
||||||
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
|
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "^3.1.2",
|
"bytes": "^3.1.2",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^2.0.0",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.1",
|
||||||
"iconv-lite": "^0.7.0",
|
"iconv-lite": "^0.7.2",
|
||||||
"on-finished": "^2.4.1",
|
"on-finished": "^2.4.1",
|
||||||
"qs": "^6.14.1",
|
"qs": "^6.15.2",
|
||||||
"raw-body": "^3.0.1",
|
"raw-body": "^3.0.2",
|
||||||
"type-is": "^2.0.1"
|
"type-is": "^2.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -6354,6 +6354,19 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/body-parser/node_modules/content-type": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
@@ -8386,9 +8399,9 @@
|
|||||||
"integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw=="
|
"integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw=="
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
|
||||||
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -9249,9 +9262,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hono": {
|
"node_modules/hono": {
|
||||||
"version": "4.12.25",
|
"version": "4.12.27",
|
||||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz",
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz",
|
||||||
"integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==",
|
"integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.9.0"
|
"node": ">=16.9.0"
|
||||||
@@ -12107,9 +12120,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.15",
|
"version": "3.3.16",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -13483,9 +13496,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.16",
|
"version": "8.5.25",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
|
||||||
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
"integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -13503,7 +13516,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.16",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
@@ -14616,9 +14629,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/seroval": {
|
"node_modules/seroval": {
|
||||||
"version": "1.4.2",
|
"version": "1.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.6.tgz",
|
||||||
"integrity": "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==",
|
"integrity": "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -14739,9 +14752,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/shell-quote": {
|
"node_modules/shell-quote": {
|
||||||
"version": "1.8.4",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz",
|
||||||
"integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
|
"integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -15916,17 +15929,34 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/type-is": {
|
"node_modules/type-is": {
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
|
||||||
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^2.0.0",
|
||||||
"media-typer": "^1.1.0",
|
"media-typer": "^1.1.0",
|
||||||
"mime-types": "^3.0.0"
|
"mime-types": "^3.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-is/node_modules/content-type": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/type-is/node_modules/mime-db": {
|
"node_modules/type-is/node_modules/mime-db": {
|
||||||
@@ -17266,9 +17296,9 @@
|
|||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/mcp": "^0.2.3",
|
"@hono/mcp": "^0.2.3",
|
||||||
"@hono/node-server": "^1.19.13",
|
"@hono/node-server": "^2.0.10",
|
||||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
"hono": "^4.12.25",
|
"hono": "^4.12.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -17276,6 +17306,18 @@
|
|||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"plugins-external/mcp-server/node_modules/@hono/node-server": {
|
||||||
|
"version": "2.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.10.tgz",
|
||||||
|
"integrity": "sha512-ZcnNVhKTmyDJeg0UlnZjvM73JBsTAuhrH/J4fjwGOw59PwOW51r4J+p6CsKZWXdKSme4MFqU62CZMOsdDrU4CA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"hono": "^4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"plugins/action-copy-curl": {
|
"plugins/action-copy-curl": {
|
||||||
"name": "@yaak/action-copy-curl",
|
"name": "@yaak/action-copy-curl",
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
|
|||||||
+1
-1
@@ -121,7 +121,7 @@
|
|||||||
"dotenv-cli": "^11.0.0",
|
"dotenv-cli": "^11.0.0",
|
||||||
"nodejs-file-downloader": "^4.13.0",
|
"nodejs-file-downloader": "^4.13.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss": "^8.5.16",
|
"postcss": "^8.5.25",
|
||||||
"tailwindcss": "^4.3.2",
|
"tailwindcss": "^4.3.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
|
"vite": "npm:@voidzero-dev/vite-plus-core@^0.2.1",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "./context";
|
} from "./context";
|
||||||
import type { TreeNode } from "./common";
|
import type { TreeNode } from "./common";
|
||||||
import { getNodeKey } from "./common";
|
import { getNodeKey } from "./common";
|
||||||
|
import { isImeCompositionEvent } from "./keyboard";
|
||||||
import type { TreeProps } from "./Tree";
|
import type { TreeProps } from "./Tree";
|
||||||
import { TreeIndentGuide } from "./TreeIndentGuide";
|
import { TreeIndentGuide } from "./TreeIndentGuide";
|
||||||
|
|
||||||
@@ -170,6 +171,8 @@ function TreeItem_<T extends { id: string }>({
|
|||||||
const handleEditKeyDown = useCallback(
|
const handleEditKeyDown = useCallback(
|
||||||
async (e: ReactKeyboardEvent<HTMLInputElement>) => {
|
async (e: ReactKeyboardEvent<HTMLInputElement>) => {
|
||||||
e.stopPropagation(); // Don't trigger other tree keys (like arrows)
|
e.stopPropagation(); // Don't trigger other tree keys (like arrows)
|
||||||
|
if (isImeCompositionEvent(e.nativeEvent)) return;
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "Enter":
|
case "Enter":
|
||||||
if (editing) {
|
if (editing) {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { describe, expect, test } from "vite-plus/test";
|
||||||
|
import { isImeCompositionEvent } from "./keyboard";
|
||||||
|
|
||||||
|
describe("isImeCompositionEvent", () => {
|
||||||
|
test("detects an active standards-based composition", () => {
|
||||||
|
expect(isImeCompositionEvent({ isComposing: true, keyCode: 13 })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects the Safari/WebKit key code fallback", () => {
|
||||||
|
expect(isImeCompositionEvent({ isComposing: false, keyCode: 229 })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not classify an ordinary Enter keydown as composition", () => {
|
||||||
|
expect(isImeCompositionEvent({ isComposing: false, keyCode: 13 })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not classify an ordinary Escape keydown as composition", () => {
|
||||||
|
expect(isImeCompositionEvent({ isComposing: false, keyCode: 27 })).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export type ImeKeyboardEvent = Pick<KeyboardEvent, "isComposing" | "keyCode">;
|
||||||
|
|
||||||
|
export function isImeCompositionEvent(event: ImeKeyboardEvent): boolean {
|
||||||
|
// Safari can clear `isComposing` on the keydown that finishes composition.
|
||||||
|
// `229` is retained as the compatibility signal that an IME is processing it.
|
||||||
|
return event.isComposing || event.keyCode === 229;
|
||||||
|
}
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/mcp": "^0.2.3",
|
"@hono/mcp": "^0.2.3",
|
||||||
"@hono/node-server": "^1.19.13",
|
"@hono/node-server": "^2.0.10",
|
||||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
"hono": "^4.12.25",
|
"hono": "^4.12.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Reference in New Issue
Block a user