mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
goose remote access (#5251)
This commit is contained in:
Generated
+120
-15
@@ -832,7 +832,7 @@ dependencies = [
|
||||
"sha1",
|
||||
"sync_wrapper 1.0.2",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"tokio-tungstenite 0.26.2",
|
||||
"tower 0.5.2",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
@@ -1992,7 +1992,16 @@ version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||
dependencies = [
|
||||
"dirs-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2003,10 +2012,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"redox_users 0.4.6",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.5"
|
||||
@@ -2633,7 +2654,7 @@ dependencies = [
|
||||
"criterion",
|
||||
"ctor",
|
||||
"dashmap",
|
||||
"dirs",
|
||||
"dirs 5.0.1",
|
||||
"dotenvy",
|
||||
"etcetera",
|
||||
"fs2",
|
||||
@@ -2853,7 +2874,9 @@ dependencies = [
|
||||
"futures",
|
||||
"goose",
|
||||
"goose-mcp",
|
||||
"hex",
|
||||
"http 1.2.0",
|
||||
"rand 0.9.2",
|
||||
"reqwest 0.12.12",
|
||||
"rmcp 0.9.0",
|
||||
"schemars",
|
||||
@@ -2861,16 +2884,19 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_yaml",
|
||||
"socket2 0.6.1",
|
||||
"tempfile",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-tungstenite 0.28.0",
|
||||
"tokio-util",
|
||||
"tower 0.5.2",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"utoipa",
|
||||
"uuid",
|
||||
"winreg 0.55.0",
|
||||
@@ -3248,7 +3274,7 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.0",
|
||||
"socket2 0.6.1",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -4174,6 +4200,23 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework 2.11.1",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-context"
|
||||
version = "0.1.1"
|
||||
@@ -5234,9 +5277,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
@@ -5382,6 +5425,17 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"libredox",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast"
|
||||
version = "1.0.24"
|
||||
@@ -6165,9 +6219,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
@@ -6201,7 +6255,7 @@ version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"dirs 6.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6295,12 +6349,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
|
||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7047,6 +7101,16 @@ dependencies = [
|
||||
"syn 2.0.99",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
@@ -7087,7 +7151,21 @@ dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"tokio",
|
||||
"tungstenite",
|
||||
"tungstenite 0.26.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tungstenite 0.28.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7446,7 +7524,25 @@ dependencies = [
|
||||
"http 1.2.0",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"sha1",
|
||||
"thiserror 2.0.12",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http 1.2.0",
|
||||
"httparse",
|
||||
"log",
|
||||
"native-tls",
|
||||
"rand 0.9.2",
|
||||
"sha1",
|
||||
"thiserror 2.0.12",
|
||||
"utf-8",
|
||||
@@ -8222,6 +8318,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
|
||||
@@ -39,7 +39,11 @@ reqwest = { version = "0.12.9", features = ["json", "rustls-tls", "blocking", "m
|
||||
tokio-util = "0.7.15"
|
||||
uuid = { version = "1.11", features = ["v4"] }
|
||||
serde_path_to_error = "0.1.20"
|
||||
winreg = { version = "0.55.0", optional = true }
|
||||
tokio-tungstenite = { version = "0.28.0", features = ["native-tls"] }
|
||||
url = "2.5.7"
|
||||
rand = "0.9.2"
|
||||
hex = "0.4.3"
|
||||
socket2 = "0.6.1"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = { version = "0.55.0" }
|
||||
|
||||
@@ -49,7 +49,7 @@ pub async fn run() -> Result<()> {
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
let app = crate::routes::configure(app_state, secret_key.clone())
|
||||
let app = crate::routes::configure(app_state.clone(), secret_key.clone())
|
||||
.layer(middleware::from_fn_with_state(
|
||||
secret_key.clone(),
|
||||
check_token,
|
||||
@@ -59,6 +59,11 @@ pub async fn run() -> Result<()> {
|
||||
let listener = tokio::net::TcpListener::bind(settings.socket_addr()).await?;
|
||||
info!("listening on {}", listener.local_addr()?);
|
||||
|
||||
let tunnel_manager = app_state.tunnel_manager.clone();
|
||||
tokio::spawn(async move {
|
||||
tunnel_manager.check_auto_start().await;
|
||||
});
|
||||
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await?;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
pub mod auth;
|
||||
pub mod configuration;
|
||||
pub mod error;
|
||||
pub mod openapi;
|
||||
pub mod routes;
|
||||
pub mod state;
|
||||
pub mod tunnel;
|
||||
|
||||
// Re-export commonly used items
|
||||
pub use openapi::*;
|
||||
|
||||
@@ -5,6 +5,7 @@ mod logging;
|
||||
mod openapi;
|
||||
mod routes;
|
||||
mod state;
|
||||
mod tunnel;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use goose::config::paths::Paths;
|
||||
|
||||
@@ -391,6 +391,9 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::recipe::parse_recipe,
|
||||
super::routes::setup::start_openrouter_setup,
|
||||
super::routes::setup::start_tetrate_setup,
|
||||
super::routes::tunnel::start_tunnel,
|
||||
super::routes::tunnel::stop_tunnel,
|
||||
super::routes::tunnel::get_tunnel_status,
|
||||
),
|
||||
components(schemas(
|
||||
super::routes::config_management::UpsertConfigQuery,
|
||||
@@ -513,6 +516,8 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::agent::AddExtensionRequest,
|
||||
super::routes::agent::RemoveExtensionRequest,
|
||||
super::routes::setup::SetupResponse,
|
||||
super::tunnel::TunnelInfo,
|
||||
super::tunnel::TunnelState,
|
||||
))
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
|
||||
@@ -10,6 +10,7 @@ pub mod schedule;
|
||||
pub mod session;
|
||||
pub mod setup;
|
||||
pub mod status;
|
||||
pub mod tunnel;
|
||||
pub mod utils;
|
||||
|
||||
use std::sync::Arc;
|
||||
@@ -28,5 +29,6 @@ pub fn configure(state: Arc<crate::state::AppState>, secret_key: String) -> Rout
|
||||
.merge(session::routes(state.clone()))
|
||||
.merge(schedule::routes(state.clone()))
|
||||
.merge(setup::routes(state.clone()))
|
||||
.merge(tunnel::routes(state.clone()))
|
||||
.merge(mcp_ui_proxy::routes(secret_key))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ErrorResponse {
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
/// Start the tunnel
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tunnel/start",
|
||||
responses(
|
||||
(status = 200, description = "Tunnel started successfully", body = TunnelInfo),
|
||||
(status = 400, description = "Bad request", body = ErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ErrorResponse)
|
||||
)
|
||||
)]
|
||||
#[axum::debug_handler]
|
||||
pub async fn start_tunnel(State(state): State<Arc<AppState>>) -> Response {
|
||||
match state.tunnel_manager.start().await {
|
||||
Ok(info) => (StatusCode::OK, Json(info)).into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to start tunnel: {}", e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: e.to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the tunnel
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/tunnel/stop",
|
||||
responses(
|
||||
(status = 200, description = "Tunnel stopped successfully"),
|
||||
(status = 500, description = "Internal server error", body = ErrorResponse)
|
||||
)
|
||||
)]
|
||||
pub async fn stop_tunnel(State(state): State<Arc<AppState>>) -> Response {
|
||||
state.tunnel_manager.stop(true).await;
|
||||
StatusCode::OK.into_response()
|
||||
}
|
||||
|
||||
/// Get tunnel info
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/tunnel/status",
|
||||
responses(
|
||||
(status = 200, description = "Tunnel info", body = TunnelInfo)
|
||||
)
|
||||
)]
|
||||
pub async fn get_tunnel_status(State(state): State<Arc<AppState>>) -> Response {
|
||||
let info = state.tunnel_manager.get_info().await;
|
||||
(StatusCode::OK, Json(info)).into_response()
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/tunnel/start", post(start_tunnel))
|
||||
.route("/tunnel/stop", post(stop_tunnel))
|
||||
.route("/tunnel/status", get(get_tunnel_status))
|
||||
.with_state(state)
|
||||
}
|
||||
@@ -6,6 +6,9 @@ use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::tunnel::TunnelManager;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub(crate) agent_manager: Arc<AgentManager>,
|
||||
@@ -13,16 +16,20 @@ pub struct AppState {
|
||||
pub session_counter: Arc<AtomicUsize>,
|
||||
/// Tracks sessions that have already emitted recipe telemetry to prevent double counting.
|
||||
recipe_session_tracker: Arc<Mutex<HashSet<String>>>,
|
||||
pub tunnel_manager: Arc<TunnelManager>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub async fn new() -> anyhow::Result<Arc<AppState>> {
|
||||
let agent_manager = AgentManager::instance().await?;
|
||||
let tunnel_manager = Arc::new(TunnelManager::new());
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
agent_manager,
|
||||
recipe_file_hash_map: Arc::new(Mutex::new(HashMap::new())),
|
||||
session_counter: Arc::new(AtomicUsize::new(0)),
|
||||
recipe_session_tracker: Arc::new(Mutex::new(HashSet::new())),
|
||||
tunnel_manager,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
use super::TunnelInfo;
|
||||
use anyhow::{Context, Result};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use reqwest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use socket2::{SockRef, TcpKeepalive};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_tungstenite::{connect_async, tungstenite::Message};
|
||||
use tracing::{error, info, warn};
|
||||
use url::Url;
|
||||
|
||||
/// Constant-time comparison using hash to prevent timing attacks
|
||||
fn secure_compare(a: &str, b: &str) -> bool {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
let mut hasher_a = DefaultHasher::new();
|
||||
a.hash(&mut hasher_a);
|
||||
let hash_a = hasher_a.finish();
|
||||
|
||||
let mut hasher_b = DefaultHasher::new();
|
||||
b.hash(&mut hasher_b);
|
||||
let hash_b = hasher_b.finish();
|
||||
|
||||
hash_a == hash_b
|
||||
}
|
||||
|
||||
const WORKER_URL: &str = "https://cloudflare-tunnel-proxy.michael-neale.workers.dev";
|
||||
const IDLE_TIMEOUT_SECS: u64 = 300;
|
||||
const CONNECTION_TIMEOUT_SECS: u64 = 30;
|
||||
const MAX_WS_SIZE: usize = 900_000;
|
||||
|
||||
fn get_worker_url() -> String {
|
||||
std::env::var("GOOSE_TUNNEL_WORKER_URL")
|
||||
.ok()
|
||||
.unwrap_or_else(|| WORKER_URL.to_string())
|
||||
}
|
||||
|
||||
type WebSocketSender = Arc<
|
||||
RwLock<
|
||||
Option<
|
||||
futures::stream::SplitSink<
|
||||
tokio_tungstenite::WebSocketStream<
|
||||
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
|
||||
>,
|
||||
Message,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct TunnelMessage {
|
||||
#[serde(rename = "requestId")]
|
||||
request_id: String,
|
||||
method: String,
|
||||
path: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
headers: Option<HashMap<String, String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct TunnelResponse {
|
||||
#[serde(rename = "requestId")]
|
||||
request_id: String,
|
||||
status: u16,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
headers: Option<HashMap<String, String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
body: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "chunkIndex")]
|
||||
chunk_index: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "totalChunks")]
|
||||
total_chunks: Option<usize>,
|
||||
#[serde(rename = "isChunked")]
|
||||
is_chunked: bool,
|
||||
#[serde(rename = "isStreaming")]
|
||||
is_streaming: bool,
|
||||
#[serde(rename = "isFirstChunk")]
|
||||
is_first_chunk: bool,
|
||||
#[serde(rename = "isLastChunk")]
|
||||
is_last_chunk: bool,
|
||||
}
|
||||
|
||||
fn validate_and_build_request(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
message: &TunnelMessage,
|
||||
tunnel_secret: &str,
|
||||
server_secret: &str,
|
||||
) -> Result<reqwest::RequestBuilder> {
|
||||
let incoming_secret = message
|
||||
.headers
|
||||
.as_ref()
|
||||
.and_then(|h| {
|
||||
h.iter()
|
||||
.find(|(k, _)| k.eq_ignore_ascii_case("x-secret-key"))
|
||||
.map(|(_, v)| v)
|
||||
})
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing tunnel secret header"))?;
|
||||
|
||||
if !secure_compare(incoming_secret, tunnel_secret) {
|
||||
anyhow::bail!("Invalid tunnel secret");
|
||||
}
|
||||
|
||||
let mut request_builder = match message.method.as_str() {
|
||||
"GET" => client.get(url),
|
||||
"POST" => client.post(url),
|
||||
"PUT" => client.put(url),
|
||||
"DELETE" => client.delete(url),
|
||||
"PATCH" => client.patch(url),
|
||||
_ => client.get(url),
|
||||
};
|
||||
|
||||
if let Some(headers) = &message.headers {
|
||||
for (key, value) in headers {
|
||||
if key.eq_ignore_ascii_case("x-secret-key") {
|
||||
continue;
|
||||
}
|
||||
request_builder = request_builder.header(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
request_builder = request_builder.header("X-Secret-Key", server_secret);
|
||||
|
||||
if let Some(body) = &message.body {
|
||||
if message.method != "GET" && message.method != "HEAD" {
|
||||
request_builder = request_builder.body(body.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(request_builder)
|
||||
}
|
||||
|
||||
async fn handle_streaming_response(
|
||||
response: reqwest::Response,
|
||||
status: u16,
|
||||
headers_map: HashMap<String, String>,
|
||||
request_id: String,
|
||||
message_path: String,
|
||||
ws_tx: WebSocketSender,
|
||||
) -> Result<()> {
|
||||
info!("← {} {} [{}] (streaming)", status, message_path, request_id);
|
||||
|
||||
let mut stream = response.bytes_stream();
|
||||
let mut chunk_index = 0;
|
||||
let mut is_first_chunk = true;
|
||||
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
match chunk_result {
|
||||
Ok(chunk) => {
|
||||
let chunk_str = String::from_utf8_lossy(&chunk).to_string();
|
||||
let tunnel_response = TunnelResponse {
|
||||
request_id: request_id.clone(),
|
||||
status,
|
||||
headers: if is_first_chunk {
|
||||
Some(headers_map.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
body: Some(chunk_str),
|
||||
error: None,
|
||||
chunk_index: Some(chunk_index),
|
||||
total_chunks: None,
|
||||
is_chunked: false,
|
||||
is_streaming: true,
|
||||
is_first_chunk,
|
||||
is_last_chunk: false,
|
||||
};
|
||||
send_response(ws_tx.clone(), tunnel_response).await?;
|
||||
chunk_index += 1;
|
||||
is_first_chunk = false;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error reading stream chunk: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tunnel_response = TunnelResponse {
|
||||
request_id: request_id.clone(),
|
||||
status,
|
||||
headers: None,
|
||||
body: Some(String::new()),
|
||||
error: None,
|
||||
chunk_index: Some(chunk_index),
|
||||
total_chunks: None,
|
||||
is_chunked: false,
|
||||
is_streaming: true,
|
||||
is_first_chunk: false,
|
||||
is_last_chunk: true,
|
||||
};
|
||||
send_response(ws_tx, tunnel_response).await?;
|
||||
info!(
|
||||
"← {} {} [{}] (complete, {} chunks)",
|
||||
status, message_path, request_id, chunk_index
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_chunked_response(
|
||||
body: String,
|
||||
status: u16,
|
||||
headers_map: HashMap<String, String>,
|
||||
request_id: String,
|
||||
message_path: String,
|
||||
ws_tx: WebSocketSender,
|
||||
) -> Result<()> {
|
||||
let total_chunks = body.len().div_ceil(MAX_WS_SIZE);
|
||||
info!(
|
||||
"← {} {} [{}] ({} bytes, {} chunks)",
|
||||
status,
|
||||
message_path,
|
||||
request_id,
|
||||
body.len(),
|
||||
total_chunks
|
||||
);
|
||||
|
||||
for (i, chunk) in body.as_bytes().chunks(MAX_WS_SIZE).enumerate() {
|
||||
let chunk_str = String::from_utf8_lossy(chunk).to_string();
|
||||
let tunnel_response = TunnelResponse {
|
||||
request_id: request_id.clone(),
|
||||
status,
|
||||
headers: if i == 0 {
|
||||
Some(headers_map.clone())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
body: Some(chunk_str),
|
||||
error: None,
|
||||
chunk_index: Some(i),
|
||||
total_chunks: Some(total_chunks),
|
||||
is_chunked: true,
|
||||
is_streaming: false,
|
||||
is_first_chunk: false,
|
||||
is_last_chunk: false,
|
||||
};
|
||||
send_response(ws_tx.clone(), tunnel_response).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_request(
|
||||
message: TunnelMessage,
|
||||
port: u16,
|
||||
ws_tx: WebSocketSender,
|
||||
tunnel_secret: String,
|
||||
server_secret: String,
|
||||
) -> Result<()> {
|
||||
let request_id = message.request_id.clone();
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let url = format!("http://127.0.0.1:{}{}", port, message.path);
|
||||
|
||||
let request_builder =
|
||||
match validate_and_build_request(&client, &url, &message, &tunnel_secret, &server_secret) {
|
||||
Ok(builder) => builder,
|
||||
Err(e) => {
|
||||
error!("✗ Authentication error [{}]: {}", request_id, e);
|
||||
let error_response = TunnelResponse {
|
||||
request_id,
|
||||
status: 401,
|
||||
headers: None,
|
||||
body: None,
|
||||
error: Some(e.to_string()),
|
||||
chunk_index: None,
|
||||
total_chunks: None,
|
||||
is_chunked: false,
|
||||
is_streaming: false,
|
||||
is_first_chunk: false,
|
||||
is_last_chunk: false,
|
||||
};
|
||||
send_response(ws_tx, error_response).await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let response = match request_builder.send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
error!("✗ Request error [{}]: {}", request_id, e);
|
||||
let error_response = TunnelResponse {
|
||||
request_id,
|
||||
status: 500,
|
||||
headers: None,
|
||||
body: None,
|
||||
error: Some(e.to_string()),
|
||||
chunk_index: None,
|
||||
total_chunks: None,
|
||||
is_chunked: false,
|
||||
is_streaming: false,
|
||||
is_first_chunk: false,
|
||||
is_last_chunk: false,
|
||||
};
|
||||
send_response(ws_tx, error_response).await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let status = response.status().as_u16();
|
||||
// Normalize header names to lowercase per RFC 7230 (HTTP headers are case-insensitive)
|
||||
let headers_map: HashMap<String, String> = response
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.as_str().to_lowercase(),
|
||||
v.to_str().unwrap_or("").to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let is_streaming = headers_map
|
||||
.get("content-type")
|
||||
.map(|ct| ct.contains("text/event-stream"))
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_streaming {
|
||||
handle_streaming_response(
|
||||
response,
|
||||
status,
|
||||
headers_map,
|
||||
request_id,
|
||||
message.path,
|
||||
ws_tx,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
|
||||
if body.len() > MAX_WS_SIZE {
|
||||
handle_chunked_response(body, status, headers_map, request_id, message.path, ws_tx)
|
||||
.await?;
|
||||
} else {
|
||||
let tunnel_response = TunnelResponse {
|
||||
request_id: request_id.clone(),
|
||||
status,
|
||||
headers: Some(headers_map),
|
||||
body: Some(body),
|
||||
error: None,
|
||||
chunk_index: None,
|
||||
total_chunks: None,
|
||||
is_chunked: false,
|
||||
is_streaming: false,
|
||||
is_first_chunk: false,
|
||||
is_last_chunk: false,
|
||||
};
|
||||
send_response(ws_tx, tunnel_response).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_response(ws_tx: WebSocketSender, response: TunnelResponse) -> Result<()> {
|
||||
let json = serde_json::to_string(&response)?;
|
||||
if let Some(tx) = ws_tx.write().await.as_mut() {
|
||||
tx.send(Message::Text(json.into()))
|
||||
.await
|
||||
.context("Failed to send response")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn configure_tcp_keepalive(
|
||||
stream: &tokio_tungstenite::WebSocketStream<
|
||||
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
|
||||
>,
|
||||
) {
|
||||
let tcp_stream = stream.get_ref().get_ref();
|
||||
let socket_ref = SockRef::from(tcp_stream);
|
||||
|
||||
let keepalive = TcpKeepalive::new()
|
||||
.with_time(Duration::from_secs(30))
|
||||
.with_interval(Duration::from_secs(30));
|
||||
|
||||
if let Err(e) = socket_ref.set_tcp_keepalive(&keepalive) {
|
||||
warn!("Failed to set TCP keep-alive: {}", e);
|
||||
} else {
|
||||
info!("✓ TCP keep-alive enabled (30s interval)");
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_websocket_messages(
|
||||
mut read: futures::stream::SplitStream<
|
||||
tokio_tungstenite::WebSocketStream<
|
||||
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
|
||||
>,
|
||||
>,
|
||||
ws_tx: WebSocketSender,
|
||||
port: u16,
|
||||
tunnel_secret: String,
|
||||
server_secret: String,
|
||||
last_activity: Arc<RwLock<Instant>>,
|
||||
active_tasks: Arc<RwLock<Vec<JoinHandle<()>>>>,
|
||||
) {
|
||||
while let Some(msg) = read.next().await {
|
||||
match msg {
|
||||
Ok(Message::Text(text)) => {
|
||||
*last_activity.write().await = Instant::now();
|
||||
|
||||
match serde_json::from_str::<TunnelMessage>(&text) {
|
||||
Ok(tunnel_msg) => {
|
||||
let ws_tx_clone = ws_tx.clone();
|
||||
let tunnel_secret_clone = tunnel_secret.clone();
|
||||
let server_secret_clone = server_secret.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
if let Err(e) = handle_request(
|
||||
tunnel_msg,
|
||||
port,
|
||||
ws_tx_clone,
|
||||
tunnel_secret_clone,
|
||||
server_secret_clone,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Error handling request: {}", e);
|
||||
}
|
||||
});
|
||||
{
|
||||
let mut tasks = active_tasks.write().await;
|
||||
tasks.retain(|t| !t.is_finished());
|
||||
tasks.push(task);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error parsing tunnel message: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Message::Close(_)) => {
|
||||
info!("✗ Connection closed by server");
|
||||
break;
|
||||
}
|
||||
Ok(Message::Ping(_)) | Ok(Message::Pong(_)) => {
|
||||
*last_activity.write().await = Instant::now();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("✗ WebSocket error: {}", e);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup_connection(
|
||||
ws_tx: WebSocketSender,
|
||||
active_tasks: Arc<RwLock<Vec<JoinHandle<()>>>>,
|
||||
) {
|
||||
if let Some(mut tx) = ws_tx.write().await.take() {
|
||||
let _ = tx.close().await;
|
||||
}
|
||||
|
||||
let tasks = active_tasks.write().await.drain(..).collect::<Vec<_>>();
|
||||
info!("Aborting {} active request tasks", tasks.len());
|
||||
for task in tasks {
|
||||
task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_single_connection(
|
||||
port: u16,
|
||||
agent_id: String,
|
||||
tunnel_secret: String,
|
||||
server_secret: String,
|
||||
restart_tx: mpsc::Sender<()>,
|
||||
) {
|
||||
let worker_url = get_worker_url();
|
||||
let ws_url = worker_url
|
||||
.replace("https://", "wss://")
|
||||
.replace("http://", "ws://");
|
||||
|
||||
let url = format!("{}/connect?agent_id={}", ws_url, agent_id);
|
||||
|
||||
info!("Connecting to {}...", url);
|
||||
|
||||
let ws_stream = match tokio::time::timeout(
|
||||
Duration::from_secs(CONNECTION_TIMEOUT_SECS),
|
||||
connect_async(url.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok((stream, _))) => {
|
||||
configure_tcp_keepalive(&stream);
|
||||
stream
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
error!("✗ WebSocket connection error: {}", e);
|
||||
let _ = restart_tx.send(()).await;
|
||||
return;
|
||||
}
|
||||
Err(_) => {
|
||||
error!(
|
||||
"✗ WebSocket connection timeout after {}s",
|
||||
CONNECTION_TIMEOUT_SECS
|
||||
);
|
||||
let _ = restart_tx.send(()).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("✓ Connected as agent: {}", agent_id);
|
||||
info!("✓ Proxying to: http://127.0.0.1:{}", port);
|
||||
let public_url = format!("{}/tunnel/{}", worker_url, agent_id);
|
||||
info!("✓ Public URL: {}", public_url);
|
||||
|
||||
let (write, read) = ws_stream.split();
|
||||
let ws_tx: WebSocketSender = Arc::new(RwLock::new(Some(write)));
|
||||
let last_activity = Arc::new(RwLock::new(Instant::now()));
|
||||
let active_tasks: Arc<RwLock<Vec<JoinHandle<()>>>> = Arc::new(RwLock::new(Vec::new()));
|
||||
|
||||
let last_activity_clone = last_activity.clone();
|
||||
let idle_task = async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
let elapsed = last_activity_clone.read().await.elapsed();
|
||||
if elapsed > Duration::from_secs(IDLE_TIMEOUT_SECS) {
|
||||
warn!(
|
||||
"No activity for {} minutes, forcing reconnect",
|
||||
IDLE_TIMEOUT_SECS / 60
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
_ = idle_task => {
|
||||
info!("✗ Idle timeout triggered");
|
||||
}
|
||||
_ = handle_websocket_messages(
|
||||
read,
|
||||
ws_tx.clone(),
|
||||
port,
|
||||
tunnel_secret.clone(),
|
||||
server_secret.clone(),
|
||||
last_activity,
|
||||
active_tasks.clone()
|
||||
) => {
|
||||
info!("✗ Connection ended");
|
||||
}
|
||||
}
|
||||
|
||||
cleanup_connection(ws_tx, active_tasks).await;
|
||||
|
||||
let _ = restart_tx.send(()).await;
|
||||
}
|
||||
|
||||
pub async fn start(
|
||||
port: u16,
|
||||
tunnel_secret: String,
|
||||
server_secret: String,
|
||||
agent_id: String,
|
||||
handle: Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>,
|
||||
restart_tx: mpsc::Sender<()>,
|
||||
) -> Result<TunnelInfo> {
|
||||
let worker_url = get_worker_url();
|
||||
|
||||
let agent_id_clone = agent_id.clone();
|
||||
let tunnel_secret_clone = tunnel_secret.clone();
|
||||
let server_secret_clone = server_secret;
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
run_single_connection(
|
||||
port,
|
||||
agent_id_clone,
|
||||
tunnel_secret_clone,
|
||||
server_secret_clone,
|
||||
restart_tx,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
*handle.write().await = Some(task);
|
||||
|
||||
let public_url = format!("{}/tunnel/{}", worker_url, agent_id);
|
||||
let hostname = Url::parse(&worker_url)?
|
||||
.host_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
Ok(TunnelInfo {
|
||||
state: super::TunnelState::Running,
|
||||
url: public_url,
|
||||
hostname,
|
||||
secret: tunnel_secret,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn stop(handle: Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>) {
|
||||
if let Some(task) = handle.write().await.take() {
|
||||
task.abort();
|
||||
info!("Lapstone tunnel stopped");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
//! Integration tests for the Lapstone HTTP tunnel
|
||||
//!
|
||||
//! These tests verify the full tunnel flow:
|
||||
//! 1. Start a local HTTP server
|
||||
//! 2. Start the tunnel (connects to real Cloudflare worker via WebSocket)
|
||||
//! 3. Make requests to the public HTTPS URL
|
||||
//! 4. Verify they proxy through to the local server
|
||||
|
||||
use super::lapstone;
|
||||
use axum::{
|
||||
extract::Request,
|
||||
response::Json,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
|
||||
const TEST_TUNNEL_SECRET: &str = "test-tunnel-secret-12345";
|
||||
const TEST_SERVER_SECRET: &str = "test-server-secret-67890";
|
||||
|
||||
async fn find_available_port() -> u16 {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("Failed to bind to port 0");
|
||||
let addr = listener.local_addr().expect("Failed to get local address");
|
||||
addr.port()
|
||||
}
|
||||
|
||||
async fn health_handler() -> Json<Value> {
|
||||
Json(json!({
|
||||
"status": "ok",
|
||||
"message": "Test server is running"
|
||||
}))
|
||||
}
|
||||
|
||||
async fn echo_handler(req: Request) -> Json<Value> {
|
||||
let headers: Vec<(String, String)> = req
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
|
||||
.collect();
|
||||
|
||||
let body = axum::body::to_bytes(req.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let body_str = String::from_utf8_lossy(&body).to_string();
|
||||
|
||||
Json(json!({
|
||||
"headers": headers,
|
||||
"body": body_str
|
||||
}))
|
||||
}
|
||||
|
||||
fn create_test_server() -> Router {
|
||||
Router::new()
|
||||
.route("/health", get(health_handler))
|
||||
.route("/echo", post(echo_handler))
|
||||
}
|
||||
|
||||
async fn start_test_http_server(port: u16) -> tokio::task::JoinHandle<()> {
|
||||
let app = create_test_server();
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
|
||||
tokio::spawn(async move {
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tunnel_end_to_end() {
|
||||
let port = find_available_port().await;
|
||||
let server_handle = start_test_http_server(port).await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||
|
||||
let handle = Arc::new(RwLock::new(None));
|
||||
let (restart_tx, _restart_rx) = mpsc::channel(1);
|
||||
|
||||
let tunnel_secret = TEST_TUNNEL_SECRET.to_string();
|
||||
let server_secret = TEST_SERVER_SECRET.to_string();
|
||||
let agent_id = super::generate_agent_id();
|
||||
|
||||
let tunnel_info = lapstone::start(
|
||||
port,
|
||||
tunnel_secret.clone(),
|
||||
server_secret.clone(),
|
||||
agent_id.clone(),
|
||||
handle.clone(),
|
||||
restart_tx,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to start tunnel");
|
||||
|
||||
let public_url = &tunnel_info.url;
|
||||
println!("Tunnel public URL: {}", public_url);
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.get(format!("{}/health", public_url))
|
||||
.header("X-Secret-Key", &tunnel_secret)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to make request to public URL");
|
||||
|
||||
assert!(
|
||||
response.status().is_success(),
|
||||
"Response status: {}",
|
||||
response.status()
|
||||
);
|
||||
let body: Value = response.json().await.expect("Failed to parse JSON");
|
||||
assert_eq!(body["status"], "ok");
|
||||
assert_eq!(body["message"], "Test server is running");
|
||||
|
||||
lapstone::stop(handle).await;
|
||||
server_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tunnel_post_request() {
|
||||
let port = find_available_port().await;
|
||||
let server_handle = start_test_http_server(port).await;
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||
|
||||
let handle = Arc::new(RwLock::new(None));
|
||||
let (restart_tx, _restart_rx) = mpsc::channel(1);
|
||||
|
||||
let tunnel_secret = TEST_TUNNEL_SECRET.to_string();
|
||||
let server_secret = TEST_SERVER_SECRET.to_string();
|
||||
let agent_id = super::generate_agent_id();
|
||||
|
||||
let tunnel_info = lapstone::start(
|
||||
port,
|
||||
tunnel_secret.clone(),
|
||||
server_secret.clone(),
|
||||
agent_id.clone(),
|
||||
handle.clone(),
|
||||
restart_tx,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to start tunnel");
|
||||
|
||||
let public_url = &tunnel_info.url;
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let test_body = json!({"test": "data", "number": 42});
|
||||
let response = client
|
||||
.post(format!("{}/echo", public_url))
|
||||
.header("X-Secret-Key", &tunnel_secret)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&test_body)
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to make POST request");
|
||||
|
||||
assert!(response.status().is_success());
|
||||
let body: Value = response.json().await.expect("Failed to parse JSON");
|
||||
assert!(body["body"].as_str().unwrap().contains("test"));
|
||||
assert!(body["body"].as_str().unwrap().contains("data"));
|
||||
assert!(body["body"].as_str().unwrap().contains("42"));
|
||||
|
||||
lapstone::stop(handle).await;
|
||||
server_handle.abort();
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
pub mod lapstone;
|
||||
|
||||
#[cfg(test)]
|
||||
mod lapstone_test;
|
||||
|
||||
use crate::configuration::Settings;
|
||||
use goose::config::Config;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
fn get_server_port() -> anyhow::Result<u16> {
|
||||
let settings = Settings::new()?;
|
||||
Ok(settings.port)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TunnelState {
|
||||
#[default]
|
||||
Idle,
|
||||
Starting,
|
||||
Running,
|
||||
Error,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TunnelInfo {
|
||||
pub state: TunnelState,
|
||||
pub url: String,
|
||||
pub hostname: String,
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
pub struct TunnelManager {
|
||||
state: Arc<RwLock<TunnelState>>,
|
||||
info: Arc<RwLock<Option<TunnelInfo>>>,
|
||||
lapstone_handle: Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>,
|
||||
restart_tx: Arc<RwLock<Option<mpsc::Sender<()>>>>,
|
||||
watchdog_handle: Arc<RwLock<Option<tokio::task::JoinHandle<()>>>>,
|
||||
}
|
||||
|
||||
impl Default for TunnelManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TunnelManager {
|
||||
pub fn new() -> Self {
|
||||
TunnelManager {
|
||||
state: Arc::new(RwLock::new(TunnelState::Idle)),
|
||||
info: Arc::new(RwLock::new(None)),
|
||||
lapstone_handle: Arc::new(RwLock::new(None)),
|
||||
restart_tx: Arc::new(RwLock::new(None)),
|
||||
watchdog_handle: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_auto_start() -> bool {
|
||||
Config::global()
|
||||
.get_param("tunnel_auto_start")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn get_secret() -> Option<String> {
|
||||
Config::global().get_secret("tunnel_secret").ok()
|
||||
}
|
||||
|
||||
fn get_agent_id() -> Option<String> {
|
||||
Config::global().get_secret("tunnel_agent_id").ok()
|
||||
}
|
||||
|
||||
pub async fn check_auto_start(&self) {
|
||||
let auto_start = Self::get_auto_start();
|
||||
let state = self.state.read().await.clone();
|
||||
|
||||
if auto_start && state == TunnelState::Idle {
|
||||
tracing::info!("Auto-starting tunnel");
|
||||
match self.start().await {
|
||||
Ok(info) => {
|
||||
tracing::info!("Tunnel auto-started successfully: {:?}", info.url);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to auto-start tunnel: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_tunnel_disabled() -> bool {
|
||||
if let Ok(val) = std::env::var("GOOSE_TUNNEL") {
|
||||
let val = val.to_lowercase();
|
||||
val == "no" || val == "none"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_info(&self) -> TunnelInfo {
|
||||
if Self::is_tunnel_disabled() {
|
||||
return TunnelInfo {
|
||||
state: TunnelState::Disabled,
|
||||
url: String::new(),
|
||||
hostname: String::new(),
|
||||
secret: String::new(),
|
||||
};
|
||||
}
|
||||
|
||||
let state = self.state.read().await.clone();
|
||||
let info = self.info.read().await.clone();
|
||||
|
||||
match info {
|
||||
Some(mut tunnel_info) => {
|
||||
tunnel_info.state = state;
|
||||
tunnel_info
|
||||
}
|
||||
None => TunnelInfo {
|
||||
state,
|
||||
url: String::new(),
|
||||
hostname: String::new(),
|
||||
secret: String::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_auto_start(auto_start: bool) -> anyhow::Result<()> {
|
||||
Config::global()
|
||||
.set_param("tunnel_auto_start", auto_start)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to save tunnel config: {}", e))
|
||||
}
|
||||
|
||||
pub fn set_secret(secret: &str) -> anyhow::Result<()> {
|
||||
Config::global()
|
||||
.set_secret("tunnel_secret", &secret.to_string())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to save tunnel secret: {}", e))
|
||||
}
|
||||
|
||||
pub fn set_agent_id(agent_id: &str) -> anyhow::Result<()> {
|
||||
Config::global()
|
||||
.set_secret("tunnel_agent_id", &agent_id.to_string())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to save tunnel agent_id: {}", e))
|
||||
}
|
||||
|
||||
async fn start_tunnel_internal(&self) -> anyhow::Result<(TunnelInfo, mpsc::Receiver<()>)> {
|
||||
let server_port = get_server_port()?;
|
||||
let tunnel_secret = Self::get_secret().unwrap_or_else(generate_secret);
|
||||
let server_secret =
|
||||
std::env::var("GOOSE_SERVER__SECRET_KEY").unwrap_or_else(|_| "test".to_string());
|
||||
let agent_id = Self::get_agent_id().unwrap_or_else(generate_agent_id);
|
||||
|
||||
Self::set_secret(&tunnel_secret)?;
|
||||
Self::set_agent_id(&agent_id)?;
|
||||
|
||||
let (restart_tx, restart_rx) = mpsc::channel::<()>(1);
|
||||
*self.restart_tx.write().await = Some(restart_tx.clone());
|
||||
|
||||
let result = lapstone::start(
|
||||
server_port,
|
||||
tunnel_secret,
|
||||
server_secret,
|
||||
agent_id,
|
||||
self.lapstone_handle.clone(),
|
||||
restart_tx,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(info) => Ok((info, restart_rx)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> anyhow::Result<TunnelInfo> {
|
||||
if Self::is_tunnel_disabled() {
|
||||
anyhow::bail!("Tunnel is disabled via GOOSE_TUNNEL environment variable");
|
||||
}
|
||||
|
||||
let mut state = self.state.write().await;
|
||||
if *state != TunnelState::Idle {
|
||||
anyhow::bail!("Tunnel is already running or starting");
|
||||
}
|
||||
*state = TunnelState::Starting;
|
||||
drop(state);
|
||||
|
||||
match self.start_tunnel_internal().await {
|
||||
Ok((info, mut restart_rx)) => {
|
||||
*self.state.write().await = TunnelState::Running;
|
||||
*self.info.write().await = Some(info.clone());
|
||||
let _ = Self::set_auto_start(true);
|
||||
|
||||
let state = self.state.clone();
|
||||
let lapstone_handle = self.lapstone_handle.clone();
|
||||
let watchdog_handle_arc = self.watchdog_handle.clone();
|
||||
let manager = Arc::new(self.clone_for_watchdog());
|
||||
|
||||
let watchdog = tokio::spawn(async move {
|
||||
while restart_rx.recv().await.is_some() {
|
||||
let auto_start = Self::get_auto_start();
|
||||
if !auto_start {
|
||||
tracing::info!("Tunnel connection lost but auto_start is disabled");
|
||||
break;
|
||||
}
|
||||
|
||||
tracing::warn!("Tunnel connection lost, initiating restart...");
|
||||
lapstone::stop(lapstone_handle.clone()).await;
|
||||
*state.write().await = TunnelState::Idle;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
*state.write().await = TunnelState::Starting;
|
||||
|
||||
match manager.start_tunnel_internal().await {
|
||||
Ok((_, new_restart_rx)) => {
|
||||
*state.write().await = TunnelState::Running;
|
||||
tracing::info!("Tunnel restarted successfully");
|
||||
restart_rx = new_restart_rx;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to restart tunnel: {}", e);
|
||||
*state.write().await = TunnelState::Error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
*watchdog_handle_arc.write().await = Some(watchdog);
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
Err(e) => {
|
||||
*self.state.write().await = TunnelState::Error;
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clone_for_watchdog(&self) -> Self {
|
||||
TunnelManager {
|
||||
state: self.state.clone(),
|
||||
info: self.info.clone(),
|
||||
lapstone_handle: self.lapstone_handle.clone(),
|
||||
restart_tx: self.restart_tx.clone(),
|
||||
watchdog_handle: self.watchdog_handle.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop(&self, clear_auto_start: bool) {
|
||||
if let Some(handle) = self.watchdog_handle.write().await.take() {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
*self.restart_tx.write().await = None;
|
||||
|
||||
lapstone::stop(self.lapstone_handle.clone()).await;
|
||||
|
||||
*self.state.write().await = TunnelState::Idle;
|
||||
*self.info.write().await = None;
|
||||
|
||||
if clear_auto_start {
|
||||
let _ = Self::set_auto_start(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_secret() -> String {
|
||||
let bytes: [u8; 32] = rand::random();
|
||||
hex::encode(bytes)
|
||||
}
|
||||
|
||||
pub(super) fn generate_agent_id() -> String {
|
||||
let bytes: [u8; 32] = rand::random();
|
||||
hex::encode(bytes)
|
||||
}
|
||||
@@ -9,3 +9,5 @@ src/bin/goose-npm/
|
||||
/test-results/
|
||||
/src/bin/temporal-service
|
||||
src/bin/temporal.db
|
||||
# Signing credentials
|
||||
.env.signing
|
||||
|
||||
@@ -2263,6 +2263,92 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tunnel/start": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::tunnel"
|
||||
],
|
||||
"summary": "Start the tunnel",
|
||||
"operationId": "start_tunnel",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Tunnel started successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TunnelInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tunnel/status": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"super::routes::tunnel"
|
||||
],
|
||||
"summary": "Get tunnel info",
|
||||
"operationId": "get_tunnel_status",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Tunnel info",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TunnelInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tunnel/stop": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::tunnel"
|
||||
],
|
||||
"summary": "Stop the tunnel",
|
||||
"operationId": "stop_tunnel",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Tunnel stopped successfully"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -5133,6 +5219,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TunnelInfo": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"state",
|
||||
"url",
|
||||
"hostname",
|
||||
"secret"
|
||||
],
|
||||
"properties": {
|
||||
"hostname": {
|
||||
"type": "string"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/TunnelState"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TunnelState": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"idle",
|
||||
"starting",
|
||||
"running",
|
||||
"error",
|
||||
"disabled"
|
||||
]
|
||||
},
|
||||
"UpdateCustomProviderRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
Generated
+221
-193
@@ -41,6 +41,7 @@
|
||||
"katex": "^0.16.25",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.546.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
@@ -125,9 +126,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ai-sdk/gateway": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.0.tgz",
|
||||
"integrity": "sha512-Gj0PuawK7NkZuyYgO/h5kDK/l6hFOjhLdTq3/Lli1FTl47iGmwhH1IZQpAL3Z09BeFYWakcwUmn02ovIm2wy9g==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.1.tgz",
|
||||
"integrity": "sha512-vPVIbnP35ZnayS937XLo85vynR85fpBQWHCdUweq7apzqFOTU2YkUd4V3msebEHbQ2Zro60ZShDDy9SMiyWTqA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
@@ -271,9 +272,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.2.tgz",
|
||||
"integrity": "sha512-ccKogJI+0aiDhOahdjANIc9SDixSud1gbwdVrhn7kMopAtLXqsz9MKmQQtIl6Y5aC2IYq+j4dz/oedL2AVMmVQ==",
|
||||
"version": "6.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.3.tgz",
|
||||
"integrity": "sha512-kiGFeY+Hxf5KbPpjRLf+ffWbkos1aGo8MBfd91oxS3O57RgU3XhZrt/6UzoVF9VMpWbC3v87SRc9jxGrc9qHtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -316,9 +317,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
|
||||
"integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
|
||||
"integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -326,22 +327,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
"@babel/generator": "^7.28.5",
|
||||
"@babel/helper-compilation-targets": "^7.27.2",
|
||||
"@babel/helper-module-transforms": "^7.28.3",
|
||||
"@babel/helpers": "^7.28.4",
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/traverse": "^7.28.4",
|
||||
"@babel/types": "^7.28.4",
|
||||
"@babel/traverse": "^7.28.5",
|
||||
"@babel/types": "^7.28.5",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
@@ -368,13 +369,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.28.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
|
||||
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
|
||||
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/types": "^7.28.5",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
@@ -470,9 +471,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -503,12 +504,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
|
||||
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
|
||||
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.4"
|
||||
"@babel/types": "^7.28.5"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -573,17 +574,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
|
||||
"integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
|
||||
"integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
"@babel/generator": "^7.28.5",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.4",
|
||||
"@babel/types": "^7.28.5",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -591,13 +592,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
|
||||
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
|
||||
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -2271,9 +2272,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/regexpp": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
|
||||
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3038,9 +3039,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz",
|
||||
"integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==",
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.2.tgz",
|
||||
"integrity": "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.6",
|
||||
@@ -5131,9 +5132,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.38",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
|
||||
"integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==",
|
||||
"version": "1.0.0-beta.43",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz",
|
||||
"integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -5488,49 +5489,49 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.15.tgz",
|
||||
"integrity": "sha512-HF4+7QxATZWY3Jr8OlZrBSXmwT3Watj0OogeDvdUY/ByXJHQ+LBtqA2brDb3sBxYslIFx6UP94BJ4X6a4L9Bmw==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz",
|
||||
"integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"enhanced-resolve": "^5.18.3",
|
||||
"jiti": "^2.6.0",
|
||||
"jiti": "^2.6.1",
|
||||
"lightningcss": "1.30.2",
|
||||
"magic-string": "^0.30.19",
|
||||
"source-map-js": "^1.2.1",
|
||||
"tailwindcss": "4.1.15"
|
||||
"tailwindcss": "4.1.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.15.tgz",
|
||||
"integrity": "sha512-krhX+UOOgnsUuks2SR7hFafXmLQrKxB4YyRTERuCE59JlYL+FawgaAlSkOYmDRJdf1Q+IFNDMl9iRnBW7QBDfQ==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz",
|
||||
"integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.15",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.15",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.15",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.15",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.15",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.15",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.15",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.15"
|
||||
"@tailwindcss/oxide-android-arm64": "4.1.16",
|
||||
"@tailwindcss/oxide-darwin-arm64": "4.1.16",
|
||||
"@tailwindcss/oxide-darwin-x64": "4.1.16",
|
||||
"@tailwindcss/oxide-freebsd-x64": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.16",
|
||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.16",
|
||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.16",
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.16",
|
||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.15.tgz",
|
||||
"integrity": "sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz",
|
||||
"integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -5545,9 +5546,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.15.tgz",
|
||||
"integrity": "sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz",
|
||||
"integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -5562,9 +5563,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.15.tgz",
|
||||
"integrity": "sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz",
|
||||
"integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5579,9 +5580,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.15.tgz",
|
||||
"integrity": "sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz",
|
||||
"integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5596,9 +5597,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.15.tgz",
|
||||
"integrity": "sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz",
|
||||
"integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -5613,9 +5614,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.15.tgz",
|
||||
"integrity": "sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz",
|
||||
"integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -5630,9 +5631,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.15.tgz",
|
||||
"integrity": "sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz",
|
||||
"integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -5647,9 +5648,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.15.tgz",
|
||||
"integrity": "sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz",
|
||||
"integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5664,9 +5665,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.15.tgz",
|
||||
"integrity": "sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz",
|
||||
"integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5681,9 +5682,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.15.tgz",
|
||||
"integrity": "sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz",
|
||||
"integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==",
|
||||
"bundleDependencies": [
|
||||
"@napi-rs/wasm-runtime",
|
||||
"@emnapi/core",
|
||||
@@ -5711,9 +5712,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.15.tgz",
|
||||
"integrity": "sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz",
|
||||
"integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -5728,9 +5729,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.15.tgz",
|
||||
"integrity": "sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz",
|
||||
"integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5758,15 +5759,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/vite": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.15.tgz",
|
||||
"integrity": "sha512-B6s60MZRTUil+xKoZoGe6i0Iar5VuW+pmcGlda2FX+guDuQ1G1sjiIy1W0frneVpeL/ZjZ4KEgWZHNrIm++2qA==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.16.tgz",
|
||||
"integrity": "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tailwindcss/node": "4.1.15",
|
||||
"@tailwindcss/oxide": "4.1.15",
|
||||
"tailwindcss": "4.1.15"
|
||||
"@tailwindcss/node": "4.1.16",
|
||||
"@tailwindcss/oxide": "4.1.16",
|
||||
"tailwindcss": "4.1.16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
@@ -5786,12 +5787,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/form-core": {
|
||||
"version": "1.24.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.24.3.tgz",
|
||||
"integrity": "sha512-e+HzSD49NWr4aIqJWtPPzmi+/phBJAP3nSPN8dvxwmJWqAxuB/cH138EcmCFf3+oA7j3BXvwvTY0I+8UweGPjQ==",
|
||||
"version": "1.24.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.24.4.tgz",
|
||||
"integrity": "sha512-+eIR7DiDamit1zvTVgaHxuIRA02YFgJaXMUGxsLRJoBpUjGl/g/nhUocQoNkRyfXqOlh8OCMTanjwDprWSRq6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/devtools-event-client": "^0.3.2",
|
||||
"@tanstack/devtools-event-client": "^0.3.3",
|
||||
"@tanstack/pacer": "^0.15.3",
|
||||
"@tanstack/store": "^0.7.7"
|
||||
},
|
||||
"funding": {
|
||||
@@ -5799,13 +5801,30 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-form": {
|
||||
"version": "1.23.7",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.23.7.tgz",
|
||||
"integrity": "sha512-p/j9Gi2+s135sOjj48RjM+6xZQr1FVpliQlETLYBEGmmmxWHgYYs2b62mTDSnuv7AqtuZhpQ+t0CRFVfbQLsFA==",
|
||||
"node_modules/@tanstack/pacer": {
|
||||
"version": "0.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/pacer/-/pacer-0.15.4.tgz",
|
||||
"integrity": "sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/form-core": "1.24.3",
|
||||
"@tanstack/devtools-event-client": "^0.3.2",
|
||||
"@tanstack/store": "^0.7.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-form": {
|
||||
"version": "1.23.8",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.23.8.tgz",
|
||||
"integrity": "sha512-ivfkiOHAI3aIWkCY4FnPWVAL6SkQWGWNVjtwIZpaoJE4ulukZWZ1KB8TQKs8f4STl+egjTsMHrWJuf2fv3Xh1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/form-core": "1.24.4",
|
||||
"@tanstack/react-store": "^0.7.7",
|
||||
"decode-formdata": "^0.9.0",
|
||||
"devalue": "^5.3.2"
|
||||
@@ -6131,9 +6150,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
|
||||
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.4.tgz",
|
||||
"integrity": "sha512-g64dbryHk7loCIrsa0R3shBnEu5p6LPJ09bu9NG58+jz+cRUjFrc3Bz0kNQ7j9bXeCsrRDvNET1G54P/GJkAyA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6372,9 +6391,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz",
|
||||
"integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6382,9 +6401,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/serve-static": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz",
|
||||
"integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==",
|
||||
"version": "1.15.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
|
||||
"integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6394,9 +6413,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/serve-static/node_modules/@types/send": {
|
||||
"version": "0.17.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
|
||||
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
|
||||
"version": "0.17.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
|
||||
"integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6705,18 +6724,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz",
|
||||
"integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==",
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz",
|
||||
"integrity": "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.28.4",
|
||||
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
||||
"@rolldown/pluginutils": "1.0.0-beta.38",
|
||||
"@rolldown/pluginutils": "1.0.0-beta.43",
|
||||
"@types/babel__core": "^7.20.5",
|
||||
"react-refresh": "^0.17.0"
|
||||
"react-refresh": "^0.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
@@ -7184,12 +7203,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ai": {
|
||||
"version": "5.0.76",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.76.tgz",
|
||||
"integrity": "sha512-ZCxi1vrpyCUnDbtYrO/W8GLvyacV9689f00yshTIQ3mFFphbD7eIv40a2AOZBv3GGRA7SSRYIDnr56wcS/gyQg==",
|
||||
"version": "5.0.80",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.80.tgz",
|
||||
"integrity": "sha512-g1o6pjxm1eTtyh295dRhsg0gvZaHFlSo2oruWrK2rIR7KafWEhNB2A2/aJ9hyPT9AMI8JnQJyto1Tl9DMqwc9w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/gateway": "2.0.0",
|
||||
"@ai-sdk/gateway": "2.0.1",
|
||||
"@ai-sdk/provider": "2.0.0",
|
||||
"@ai-sdk/provider-utils": "3.0.12",
|
||||
"@opentelemetry/api": "1.9.0"
|
||||
@@ -7488,9 +7507,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul": {
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.7.tgz",
|
||||
"integrity": "sha512-kr1Hy6YRZBkGQSb6puP+D6FQ59Cx4m0siYhAxygMCAgadiWQ6oxAxQXHOMvJx67SJ63jRoVIIg5eXzUbbct1ww==",
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz",
|
||||
"integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7644,9 +7663,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.19.tgz",
|
||||
"integrity": "sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==",
|
||||
"version": "2.8.20",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz",
|
||||
"integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -7736,9 +7755,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.26.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
|
||||
"integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
|
||||
"version": "4.27.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
|
||||
"integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -7757,11 +7776,11 @@
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.9",
|
||||
"caniuse-lite": "^1.0.30001746",
|
||||
"electron-to-chromium": "^1.5.227",
|
||||
"node-releases": "^2.0.21",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
"electron-to-chromium": "^1.5.238",
|
||||
"node-releases": "^2.0.26",
|
||||
"update-browserslist-db": "^1.1.4"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
@@ -8610,9 +8629,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cronstrue": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-3.3.0.tgz",
|
||||
"integrity": "sha512-iwJytzJph1hosXC09zY8F5ACDJKerr0h3/2mOxg9+5uuFObYlgK0m35uUPk4GCvhHc2abK7NfnR9oMqY0qZFAg==",
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-3.9.0.tgz",
|
||||
"integrity": "sha512-T3S35zmD0Ai2B4ko6+mEM+k9C6tipe2nB9RLiGT6QL2Wn0Vsn2cCZAC8Oeuf4CaE00GZWVdpYitbpWCNlIWqdA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cronstrue": "bin/cli.js"
|
||||
@@ -9044,9 +9063,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.4.1.tgz",
|
||||
"integrity": "sha512-YtoaOfsqjbZQKGIMRYDWKjUmSB4VJ/RElB+bXZawQAQYAo4xu08GKTMVlsZDTF6R2MbAgjcAQRPI5eIyRAT2OQ==",
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.4.2.tgz",
|
||||
"integrity": "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
@@ -9143,9 +9162,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "38.3.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-38.3.0.tgz",
|
||||
"integrity": "sha512-Wij4AzX4SAV0X/ktq+NrWrp5piTCSS8F6YWh1KAcG+QRtNzyns9XLKERP68nFHIwfprhxF2YCN2uj7nx9DaeJw==",
|
||||
"version": "38.4.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-38.4.0.tgz",
|
||||
"integrity": "sha512-9CsXKbGf2qpofVe2pQYSgom2E//zLDJO2rGLLbxgy9tkdTOs7000Gte+d/PUtzLjI/DS95jDK0ojYAeqjLvpYg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -9650,9 +9669,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.237",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz",
|
||||
"integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==",
|
||||
"version": "1.5.240",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz",
|
||||
"integrity": "sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -13174,9 +13193,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/knip": {
|
||||
"version": "5.66.2",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-5.66.2.tgz",
|
||||
"integrity": "sha512-5wvsdc17C5bMxjuGfN9KVS/tW5KIvzP1RClfpTMdLYm8IXIsfWsiHlFkTvZIca9skwoVDyTyXmbRq4w1Poim+A==",
|
||||
"version": "5.66.3",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-5.66.3.tgz",
|
||||
"integrity": "sha512-BEe9ZCI8fm4TJzehnrCt+L/Faqu6qfMH6VrwSfck+lCGotQzf0jh5dVXysPWjWqMpdUSr6+MpMu9JW/G6wiAcQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -13536,14 +13555,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lint-staged": {
|
||||
"version": "16.2.5",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.5.tgz",
|
||||
"integrity": "sha512-o36wH3OX0jRWqDw5dOa8a8x6GXTKaLM+LvhRaucZxez0IxA+KNDUCiyjBfNgsMNmchwSX6urLSL7wShcUqAang==",
|
||||
"version": "16.2.6",
|
||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.6.tgz",
|
||||
"integrity": "sha512-s1gphtDbV4bmW1eylXpVMk2u7is7YsrLl8hzrtvC70h4ByhcMLZFY01Fx05ZUDNuv1H8HO4E+e2zgejV1jVwNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^14.0.1",
|
||||
"listr2": "^9.0.4",
|
||||
"listr2": "^9.0.5",
|
||||
"micromatch": "^4.0.8",
|
||||
"nano-spawn": "^2.0.0",
|
||||
"pidtree": "^0.6.0",
|
||||
@@ -13619,9 +13638,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lint-staged/node_modules/cli-truncate": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.0.tgz",
|
||||
"integrity": "sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
|
||||
"integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -13636,9 +13655,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lint-staged/node_modules/commander": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz",
|
||||
"integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
|
||||
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -14205,9 +14224,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.19",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -16861,6 +16880,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode.react": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
|
||||
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
@@ -17115,9 +17143,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||
"integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -19060,9 +19088,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.1.15",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz",
|
||||
"integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==",
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
|
||||
"integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
@@ -19904,9 +19932,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -20106,9 +20134,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
|
||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||
"version": "7.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"katex": "^0.16.25",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.546.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Client, Options as Options2, TDataShape } from './client';
|
||||
import { client } from './client.gen';
|
||||
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StatusData, StatusResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
|
||||
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CheckProviderData, ConfirmPermissionData, ConfirmPermissionErrors, ConfirmPermissionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||
/**
|
||||
@@ -604,3 +604,33 @@ export const status = <ThrowOnError extends boolean = false>(options?: Options<S
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the tunnel
|
||||
*/
|
||||
export const startTunnel = <ThrowOnError extends boolean = false>(options?: Options<StartTunnelData, ThrowOnError>) => {
|
||||
return (options?.client ?? client).post<StartTunnelResponses, StartTunnelErrors, ThrowOnError>({
|
||||
url: '/tunnel/start',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get tunnel info
|
||||
*/
|
||||
export const getTunnelStatus = <ThrowOnError extends boolean = false>(options?: Options<GetTunnelStatusData, ThrowOnError>) => {
|
||||
return (options?.client ?? client).get<GetTunnelStatusResponses, unknown, ThrowOnError>({
|
||||
url: '/tunnel/status',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop the tunnel
|
||||
*/
|
||||
export const stopTunnel = <ThrowOnError extends boolean = false>(options?: Options<StopTunnelData, ThrowOnError>) => {
|
||||
return (options?.client ?? client).post<StopTunnelResponses, StopTunnelErrors, ThrowOnError>({
|
||||
url: '/tunnel/stop',
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
@@ -921,6 +921,15 @@ export type ToolResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TunnelInfo = {
|
||||
hostname: string;
|
||||
secret: string;
|
||||
state: TunnelState;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type TunnelState = 'idle' | 'starting' | 'running' | 'error' | 'disabled';
|
||||
|
||||
export type UpdateCustomProviderRequest = {
|
||||
api_key: string;
|
||||
api_url: string;
|
||||
@@ -2728,3 +2737,71 @@ export type StatusResponses = {
|
||||
};
|
||||
|
||||
export type StatusResponse = StatusResponses[keyof StatusResponses];
|
||||
|
||||
export type StartTunnelData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/tunnel/start';
|
||||
};
|
||||
|
||||
export type StartTunnelErrors = {
|
||||
/**
|
||||
* Bad request
|
||||
*/
|
||||
400: ErrorResponse;
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: ErrorResponse;
|
||||
};
|
||||
|
||||
export type StartTunnelError = StartTunnelErrors[keyof StartTunnelErrors];
|
||||
|
||||
export type StartTunnelResponses = {
|
||||
/**
|
||||
* Tunnel started successfully
|
||||
*/
|
||||
200: TunnelInfo;
|
||||
};
|
||||
|
||||
export type StartTunnelResponse = StartTunnelResponses[keyof StartTunnelResponses];
|
||||
|
||||
export type GetTunnelStatusData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/tunnel/status';
|
||||
};
|
||||
|
||||
export type GetTunnelStatusResponses = {
|
||||
/**
|
||||
* Tunnel info
|
||||
*/
|
||||
200: TunnelInfo;
|
||||
};
|
||||
|
||||
export type GetTunnelStatusResponse = GetTunnelStatusResponses[keyof GetTunnelStatusResponses];
|
||||
|
||||
export type StopTunnelData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/tunnel/stop';
|
||||
};
|
||||
|
||||
export type StopTunnelErrors = {
|
||||
/**
|
||||
* Internal server error
|
||||
*/
|
||||
500: ErrorResponse;
|
||||
};
|
||||
|
||||
export type StopTunnelError = StopTunnelErrors[keyof StopTunnelErrors];
|
||||
|
||||
export type StopTunnelResponses = {
|
||||
/**
|
||||
* Tunnel stopped successfully
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button } from '../../ui/button';
|
||||
import { Settings, RefreshCw, ExternalLink } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog';
|
||||
import UpdateSection from './UpdateSection';
|
||||
import TunnelSection from '../tunnel/TunnelSection';
|
||||
import { COST_TRACKING_ENABLED, UPDATES_ENABLED } from '../../../updates';
|
||||
import { getApiUrl } from '../../../config';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
|
||||
@@ -394,6 +395,8 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<TunnelSection />
|
||||
|
||||
<Card className="rounded-lg">
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle className="mb-1">Help & feedback</CardTitle>
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../../ui/dialog';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import {
|
||||
Loader2,
|
||||
Copy,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Info,
|
||||
ExternalLink,
|
||||
QrCode,
|
||||
} from 'lucide-react';
|
||||
import { errorMessage } from '../../../utils/conversionUtils';
|
||||
import { startTunnel, stopTunnel, getTunnelStatus } from '../../../api/sdk.gen';
|
||||
import type { TunnelInfo } from '../../../api/types.gen';
|
||||
|
||||
const STATUS_MESSAGES = {
|
||||
idle: 'Tunnel is not running',
|
||||
starting: 'Starting tunnel...',
|
||||
running: 'Tunnel is active',
|
||||
error: 'Tunnel encountered an error',
|
||||
disabled: 'Tunnel is disabled',
|
||||
} as const;
|
||||
|
||||
const IOS_APP_STORE_URL = 'https://apps.apple.com/us/app/goose-ai/id6752889295';
|
||||
|
||||
export default function TunnelSection() {
|
||||
const [tunnelInfo, setTunnelInfo] = useState<TunnelInfo>({
|
||||
state: 'idle',
|
||||
url: '',
|
||||
hostname: '',
|
||||
secret: '',
|
||||
});
|
||||
const [showQRModal, setShowQRModal] = useState(false);
|
||||
const [showAppStoreQRModal, setShowAppStoreQRModal] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [copiedUrl, setCopiedUrl] = useState(false);
|
||||
const [copiedSecret, setCopiedSecret] = useState(false);
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadTunnelInfo = async () => {
|
||||
try {
|
||||
const { data } = await getTunnelStatus();
|
||||
if (data) {
|
||||
setTunnelInfo(data);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = errorMessage(err, 'Failed to load tunnel status');
|
||||
setError(errorMsg);
|
||||
setTunnelInfo({ state: 'error', url: '', hostname: '', secret: '' });
|
||||
}
|
||||
};
|
||||
|
||||
loadTunnelInfo();
|
||||
}, []);
|
||||
|
||||
const handleToggleTunnel = async () => {
|
||||
if (tunnelInfo.state === 'running') {
|
||||
try {
|
||||
await stopTunnel();
|
||||
setTunnelInfo({ state: 'idle', url: '', hostname: '', secret: '' });
|
||||
setShowQRModal(false);
|
||||
} catch (err) {
|
||||
setError(errorMessage(err, 'Failed to stop tunnel'));
|
||||
try {
|
||||
const { data } = await getTunnelStatus();
|
||||
if (data) {
|
||||
setTunnelInfo(data);
|
||||
}
|
||||
} catch (statusErr) {
|
||||
console.error('Failed to fetch tunnel status after stop error:', statusErr);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setError(null);
|
||||
setTunnelInfo({ state: 'starting', url: '', hostname: '', secret: '' });
|
||||
|
||||
try {
|
||||
const { data } = await startTunnel();
|
||||
if (data) {
|
||||
setTunnelInfo(data);
|
||||
setShowQRModal(true);
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = errorMessage(err, 'Failed to start tunnel');
|
||||
setError(errorMsg);
|
||||
setTunnelInfo({ state: 'error', url: '', hostname: '', secret: '' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text: string, type: 'url' | 'secret') => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (type === 'url') {
|
||||
setCopiedUrl(true);
|
||||
setTimeout(() => setCopiedUrl(false), 2000);
|
||||
} else {
|
||||
setCopiedSecret(true);
|
||||
setTimeout(() => setCopiedSecret(false), 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to copy to clipboard:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const getQRCodeData = () => {
|
||||
if (tunnelInfo.state !== 'running') return '';
|
||||
|
||||
const configJson = JSON.stringify({
|
||||
url: tunnelInfo.url,
|
||||
secret: tunnelInfo.secret,
|
||||
});
|
||||
const urlEncodedConfig = encodeURIComponent(configJson);
|
||||
return `goosechat://configure?data=${urlEncodedConfig}`;
|
||||
};
|
||||
|
||||
if (tunnelInfo.state === 'disabled') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="rounded-lg">
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle className="mb-1">Remote Access</CardTitle>
|
||||
<CardDescription className="flex flex-col gap-2">
|
||||
<div className="flex items-start gap-2 p-2 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded">
|
||||
<Info className="h-4 w-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-xs text-blue-800 dark:text-blue-200">
|
||||
<strong>Preview feature:</strong> Enable remote access to goose from mobile devices
|
||||
using secure tunneling.{' '}
|
||||
<a
|
||||
href={IOS_APP_STORE_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 underline hover:no-underline"
|
||||
>
|
||||
Get the iOS app
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
{' or '}
|
||||
<button
|
||||
onClick={() => setShowAppStoreQRModal(true)}
|
||||
className="inline-flex items-center gap-1 underline hover:no-underline"
|
||||
>
|
||||
scan QR code
|
||||
<QrCode className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 px-4 space-y-4">
|
||||
{error && (
|
||||
<div className="p-3 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded text-sm text-red-800 dark:text-red-200">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-text-default text-xs">Tunnel Status</h3>
|
||||
<p className="text-xs text-text-muted max-w-md mt-[2px]">
|
||||
{STATUS_MESSAGES[tunnelInfo.state]}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{tunnelInfo.state === 'starting' ? (
|
||||
<Button disabled variant="secondary" size="sm">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Starting...
|
||||
</Button>
|
||||
) : tunnelInfo.state === 'running' ? (
|
||||
<>
|
||||
<Button onClick={() => setShowQRModal(true)} variant="default" size="sm">
|
||||
Show QR Code
|
||||
</Button>
|
||||
<Button onClick={handleToggleTunnel} variant="destructive" size="sm">
|
||||
Stop Tunnel
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={handleToggleTunnel} variant="default" size="sm">
|
||||
{tunnelInfo.state === 'error' ? 'Retry' : 'Start Tunnel'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tunnelInfo.state === 'running' && (
|
||||
<div className="p-3 bg-green-100 dark:bg-green-900/20 border border-green-300 dark:border-green-800 rounded">
|
||||
<p className="text-xs text-green-800 dark:text-green-200">
|
||||
<strong>URL:</strong> {tunnelInfo.url}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Dialog open={showQRModal} onOpenChange={setShowQRModal}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Remote Access Connection</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{tunnelInfo.state === 'running' && (
|
||||
<div className="py-4 space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<div className="p-4 bg-white rounded-lg">
|
||||
<QRCodeSVG value={getQRCodeData()} size={200} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-text-muted">
|
||||
Scan this QR code with the goose mobile app. Do not share this code with anyone else
|
||||
as it is for your personal access.
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<button
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
className="flex items-center justify-between w-full text-sm font-medium hover:opacity-70 transition-opacity"
|
||||
>
|
||||
<span>Connection Details</span>
|
||||
{showDetails ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{showDetails && (
|
||||
<div className="mt-3 space-y-3">
|
||||
<div>
|
||||
<h3 className="text-xs font-medium mb-1 text-text-muted">Tunnel URL</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs break-all overflow-hidden">
|
||||
{tunnelInfo.url}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="flex-shrink-0"
|
||||
onClick={() => tunnelInfo.url && copyToClipboard(tunnelInfo.url, 'url')}
|
||||
>
|
||||
{copiedUrl ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xs font-medium mb-1 text-text-muted">Secret Key</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs break-all overflow-hidden">
|
||||
{tunnelInfo.secret}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="flex-shrink-0"
|
||||
onClick={() =>
|
||||
tunnelInfo.secret && copyToClipboard(tunnelInfo.secret, 'secret')
|
||||
}
|
||||
>
|
||||
{copiedSecret ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowQRModal(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleToggleTunnel}>
|
||||
Stop Tunnel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={showAppStoreQRModal} onOpenChange={setShowAppStoreQRModal}>
|
||||
<DialogContent className="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Download goose iOS App</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4 space-y-4">
|
||||
<div className="flex justify-center">
|
||||
<div className="p-4 bg-white rounded-lg">
|
||||
<QRCodeSVG value={IOS_APP_STORE_URL} size={200} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-text-muted">
|
||||
Scan this QR code with your iPhone camera to install the goose mobile app from the App
|
||||
Store
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<a
|
||||
href={IOS_APP_STORE_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
Open in App Store
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowAppStoreQRModal(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1788,7 +1788,6 @@ async function appMain() {
|
||||
app.dock?.hide();
|
||||
}
|
||||
|
||||
// Parse command line arguments
|
||||
const { dirPath } = parseArgs();
|
||||
|
||||
await createNewWindow(app, dirPath);
|
||||
|
||||
@@ -6,6 +6,7 @@ export default defineConfig({
|
||||
define: {
|
||||
// This replaces process.env.ALPHA with a literal at build time
|
||||
'process.env.ALPHA': JSON.stringify(process.env.ALPHA === 'true'),
|
||||
'process.env.GOOSE_TUNNEL': JSON.stringify(process.env.GOOSE_TUNNEL !== 'no' && process.env.GOOSE_TUNNEL !== 'none'),
|
||||
},
|
||||
|
||||
plugins: [tailwindcss()],
|
||||
|
||||
Reference in New Issue
Block a user