From f5d5af30581b9bb7d1b917b16fd021f692b90c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar?= Date: Thu, 9 Apr 2026 09:34:09 +0200 Subject: [PATCH 1/4] refactor: merge session creation for local and preview --- linkup-cli/src/commands/preview.rs | 8 +-- linkup-cli/src/state.rs | 4 +- linkup-cli/src/worker_client.rs | 6 +- linkup/src/session.rs | 97 +++++++++++++++--------------- local-server/src/lib.rs | 19 ++++-- server-tests/tests/helpers.rs | 4 +- server-tests/tests/server_test.rs | 4 +- worker/src/lib.rs | 18 +++--- 8 files changed, 87 insertions(+), 73 deletions(-) diff --git a/linkup-cli/src/commands/preview.rs b/linkup-cli/src/commands/preview.rs index 4436b7a7..c9d4a9d7 100644 --- a/linkup-cli/src/commands/preview.rs +++ b/linkup-cli/src/commands/preview.rs @@ -4,7 +4,7 @@ use crate::state::{config_path, get_config}; use crate::worker_client::WorkerClient; use anyhow::Context; use clap::builder::ValueParser; -use linkup::CreatePreviewRequest; +use linkup::UpsertSessionRequest; use url::Url; #[derive(clap::Args)] @@ -24,12 +24,12 @@ pub struct Args { pub async fn preview(args: &Args, config: &Option) -> Result<()> { let config_path = config_path(config)?; let input_config = get_config(&config_path)?; - let create_preview_request: CreatePreviewRequest = + let upsert_session_request: UpsertSessionRequest = linkup::create_preview_req_from_config(&input_config, &args.services); let url = input_config.linkup.worker_url.clone(); if args.print_request { - let create_req_json = serde_json::to_string(&create_preview_request) + let create_req_json = serde_json::to_string(&upsert_session_request) .context("Failed to encode request to JSON string")?; println!("{}", create_req_json); @@ -38,7 +38,7 @@ pub async fn preview(args: &Args, config: &Option) -> Result<()> { } let preview_name = WorkerClient::from(&input_config) - .preview(&create_preview_request) + .preview(&upsert_session_request) .await .with_context(|| format!("Failed to send preview request to {}", url))?; diff --git a/linkup-cli/src/state.rs b/linkup-cli/src/state.rs index 52697bed..d2e8a4b2 100644 --- a/linkup-cli/src/state.rs +++ b/linkup-cli/src/state.rs @@ -10,7 +10,7 @@ use regex::Regex; use serde::{Deserialize, Serialize}; use url::Url; -use linkup::{Domain, Session, SessionService, UpdateSessionRequest}; +use linkup::{Domain, Session, SessionService, UpsertSessionRequest}; use crate::{ LINKUP_CONFIG_ENV, LINKUP_STATE_FILE, Result, linkup_file_path, services, @@ -233,7 +233,7 @@ async fn upload_session_to_server( desired_name: &str, session: Session, ) -> Result { - let session_update_req = UpdateSessionRequest { + let session_update_req = UpsertSessionRequest::Named { session_token: session.session_token, desired_name: desired_name.to_string(), services: session.services, diff --git a/linkup-cli/src/worker_client.rs b/linkup-cli/src/worker_client.rs index 9f34d216..4e8f6ef7 100644 --- a/linkup-cli/src/worker_client.rs +++ b/linkup-cli/src/worker_client.rs @@ -1,4 +1,4 @@ -use linkup::{CreatePreviewRequest, UpdateSessionRequest}; +use linkup::UpsertSessionRequest; use reqwest::{StatusCode, header}; use serde::{Deserialize, Serialize}; use url::Url; @@ -62,11 +62,11 @@ impl WorkerClient { } } - pub async fn preview(&self, params: &CreatePreviewRequest) -> Result { + pub async fn preview(&self, params: &UpsertSessionRequest) -> Result { self.post("/linkup/preview-session", params).await } - pub async fn linkup(&self, params: &UpdateSessionRequest) -> Result { + pub async fn linkup(&self, params: &UpsertSessionRequest) -> Result { self.post("/linkup/local-session", params).await } diff --git a/linkup/src/session.rs b/linkup/src/session.rs index 8b2aaefb..ef3e127a 100644 --- a/linkup/src/session.rs +++ b/linkup/src/session.rs @@ -27,29 +27,30 @@ pub struct Route { } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct UpdateSessionRequest { - pub desired_name: String, - pub session_token: String, - pub services: Vec, - pub domains: Vec, - #[serde( - default, - serialize_with = "crate::serde_ext::serialize_opt_vec_regex", - deserialize_with = "crate::serde_ext::deserialize_opt_vec_regex" - )] - pub cache_routes: Option>, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CreatePreviewRequest { - pub services: Vec, - pub domains: Vec, - #[serde( - default, - serialize_with = "crate::serde_ext::serialize_opt_vec_regex", - deserialize_with = "crate::serde_ext::deserialize_opt_vec_regex" - )] - pub cache_routes: Option>, +#[serde(untagged)] +pub enum UpsertSessionRequest { + Named { + desired_name: String, + session_token: String, + services: Vec, + domains: Vec, + #[serde( + default, + serialize_with = "crate::serde_ext::serialize_opt_vec_regex", + deserialize_with = "crate::serde_ext::deserialize_opt_vec_regex" + )] + cache_routes: Option>, + }, + Unnamed { + services: Vec, + domains: Vec, + #[serde( + default, + serialize_with = "crate::serde_ext::serialize_opt_vec_regex", + deserialize_with = "crate::serde_ext::deserialize_opt_vec_regex" + )] + cache_routes: Option>, + }, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -112,33 +113,35 @@ impl Session { } } -impl TryFrom for Session { +impl TryFrom for Session { type Error = ConfigError; - fn try_from(req: UpdateSessionRequest) -> Result { - let session = Self { - session_token: req.session_token, - services: req.services, - domains: req.domains, - cache_routes: req.cache_routes, + fn try_from(req: UpsertSessionRequest) -> Result { + let (session_token, services, domains, cache_routes) = match req { + UpsertSessionRequest::Named { + services, + domains, + cache_routes, + session_token, + .. + } => (session_token, services, domains, cache_routes), + UpsertSessionRequest::Unnamed { + services, + domains, + cache_routes, + } => ( + PREVIEW_SESSION_TOKEN.to_string(), + services, + domains, + cache_routes, + ), }; - validate_not_empty(&session)?; - validate_services(&session)?; - - Ok(session) - } -} - -impl TryFrom for Session { - type Error = ConfigError; - - fn try_from(req: CreatePreviewRequest) -> Result { let session = Self { - session_token: PREVIEW_SESSION_TOKEN.to_string(), - services: req.services, - domains: req.domains, - cache_routes: req.cache_routes, + session_token, + services, + domains, + cache_routes, }; validate_not_empty(&session)?; @@ -164,7 +167,7 @@ impl TryFrom for Session { pub fn create_preview_req_from_config( config: &Config, services_overwrite: &[(String, Url)], -) -> CreatePreviewRequest { +) -> UpsertSessionRequest { let mut session_services: Vec = Vec::with_capacity(config.services.len()); for service in &config.services { @@ -184,7 +187,7 @@ pub fn create_preview_req_from_config( }); } - CreatePreviewRequest { + UpsertSessionRequest::Unnamed { services: session_services, domains: config.domains.clone(), cache_routes: config.linkup.cache_routes.clone(), diff --git a/local-server/src/lib.rs b/local-server/src/lib.rs index 8fd22ff3..fc83dacf 100644 --- a/local-server/src/lib.rs +++ b/local-server/src/lib.rs @@ -31,7 +31,7 @@ use hyper_util::{ rt::TokioExecutor, }; use linkup::{ - MemoryStringStore, NameKind, Session, SessionAllocator, TargetService, UpdateSessionRequest, + MemoryStringStore, NameKind, Session, SessionAllocator, TargetService, UpsertSessionRequest, allow_all_cors, get_additional_headers, get_target_service, }; use rustls::ServerConfig; @@ -417,16 +417,23 @@ async fn handle_http_req( async fn linkup_config_handler( Extension(store): Extension, Extension(dns_catalog): Extension, - Json(update_req): Json, + Json(upsert_req): Json, ) -> impl IntoResponse { - let desired_name = update_req.desired_name.clone(); - let domains = update_req - .domains + let (desired_name, req_domains) = match &upsert_req { + UpsertSessionRequest::Named { + desired_name, + domains, + .. + } => (desired_name.clone(), domains), + UpsertSessionRequest::Unnamed { domains, .. } => (String::new(), domains), + }; + + let domains = req_domains .iter() .map(|domain| domain.domain.clone()) .collect::>(); - let server_conf: Session = match update_req.try_into() { + let server_conf: Session = match upsert_req.try_into() { Ok(conf) => conf, Err(e) => { return ApiError::new( diff --git a/server-tests/tests/helpers.rs b/server-tests/tests/helpers.rs index b37605ad..fba067b1 100644 --- a/server-tests/tests/helpers.rs +++ b/server-tests/tests/helpers.rs @@ -1,6 +1,6 @@ use std::process::Command; -use linkup::{Domain, MemoryStringStore, SessionService, UpdateSessionRequest}; +use linkup::{Domain, MemoryStringStore, SessionService, UpsertSessionRequest}; use linkup_local_server::{DnsCatalog, linkup_router}; use reqwest::Url; use tokio::net::TcpListener; @@ -54,7 +54,7 @@ pub fn create_session_request(name: String, fe_location: Option) -> Stri Some(location) => location, None => "http://example.com".to_string(), }; - let req = UpdateSessionRequest { + let req = UpsertSessionRequest::Named { desired_name: name, session_token: "token".to_string(), domains: vec![Domain { diff --git a/server-tests/tests/server_test.rs b/server-tests/tests/server_test.rs index 2ca38cdc..cb4165b4 100644 --- a/server-tests/tests/server_test.rs +++ b/server-tests/tests/server_test.rs @@ -1,5 +1,5 @@ use helpers::ServerKind; -use linkup::{CreatePreviewRequest, Domain, SessionService}; +use linkup::{Domain, SessionService, UpsertSessionRequest}; use reqwest::Url; use rstest::rstest; @@ -84,7 +84,7 @@ pub fn create_preview_request(fe_location: Option) -> String { Some(location) => location, None => "http://example.com".to_string(), }; - let req = CreatePreviewRequest { + let req = UpsertSessionRequest::Unnamed { domains: vec![Domain { domain: "example.com".to_string(), default_service: "frontend".to_string(), diff --git a/worker/src/lib.rs b/worker/src/lib.rs index e473d99e..593c885e 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -11,8 +11,8 @@ use http::{HeaderMap, Uri}; use http_error::HttpError; use kv_store::CfWorkerStringStore; use linkup::{ - CreatePreviewRequest, NameKind, Session, SessionAllocator, UpdateSessionRequest, Version, - VersionChannel, allow_all_cors, get_additional_headers, get_target_service, + NameKind, Session, SessionAllocator, UpsertSessionRequest, Version, VersionChannel, + allow_all_cors, get_additional_headers, get_target_service, }; use serde::{Deserialize, Serialize}; use tower_service::Service; @@ -198,13 +198,17 @@ async fn get_tunnel_handler( #[worker::send] async fn linkup_session_handler( State(state): State, - Json(update_req): Json, + Json(upsert_req): Json, ) -> impl IntoResponse { let store = CfWorkerStringStore::new(state.sessions_kv.clone()); let sessions = SessionAllocator::new(&store); - let desired_name = update_req.desired_name.clone(); - let server_conf: Session = match update_req.try_into() { + let desired_name = match &upsert_req { + UpsertSessionRequest::Named { desired_name, .. } => desired_name.clone(), + UpsertSessionRequest::Unnamed { .. } => String::new(), + }; + + let server_conf: Session = match upsert_req.try_into() { Ok(conf) => conf, Err(e) => { return HttpError::new( @@ -236,12 +240,12 @@ async fn linkup_session_handler( #[worker::send] async fn linkup_preview_handler( State(state): State, - Json(update_req): Json, + Json(upsert_req): Json, ) -> impl IntoResponse { let store = CfWorkerStringStore::new(state.sessions_kv.clone()); let sessions = SessionAllocator::new(&store); - let server_conf: Session = match update_req.try_into() { + let server_conf: Session = match upsert_req.try_into() { Ok(conf) => conf, Err(e) => { return HttpError::new( From 9dd197ed250ade0f280a0d060603ff7b2c5fb18d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar?= Date: Thu, 9 Apr 2026 10:00:25 +0200 Subject: [PATCH 2/4] feat: allow preview session to be updated --- worker/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 593c885e..371752ee 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -245,6 +245,11 @@ async fn linkup_preview_handler( let store = CfWorkerStringStore::new(state.sessions_kv.clone()); let sessions = SessionAllocator::new(&store); + let desired_name = match &upsert_req { + UpsertSessionRequest::Named { desired_name, .. } => desired_name.clone(), + UpsertSessionRequest::Unnamed { .. } => String::new(), + }; + let server_conf: Session = match upsert_req.try_into() { Ok(conf) => conf, Err(e) => { @@ -257,7 +262,7 @@ async fn linkup_preview_handler( }; let session_name = sessions - .store_session(server_conf, NameKind::SixChar, String::from("")) + .store_session(server_conf, NameKind::SixChar, desired_name) .await; let name = match session_name { From 647f6f5fdfb192716b2aa78d73a1925489b6bc7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar?= Date: Mon, 13 Apr 2026 08:03:47 +0200 Subject: [PATCH 3/4] refactor: unify preview and local session handlers --- worker/src/lib.rs | 55 +++++++++++++++-------------------------------- 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 6ee6fb1b..ca594ae1 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -199,57 +199,36 @@ async fn linkup_session_handler( State(state): State, Json(upsert_req): Json, ) -> impl IntoResponse { - let store = CfWorkerStringStore::new(state.sessions_kv.clone()); - let sessions = SessionAllocator::new(&store); - - let desired_name = match &upsert_req { - UpsertSessionRequest::Named { desired_name, .. } => desired_name.clone(), - UpsertSessionRequest::Unnamed { .. } => String::new(), - }; - - let server_conf: Session = match upsert_req.try_into() { - Ok(conf) => conf, - Err(e) => { - return HttpError::new( - format!("Failed to parse server config: {} - Worker", e), - StatusCode::BAD_REQUEST, - ) - .into_response(); - } - }; - - let session_name = sessions - .store_session(server_conf, NameKind::Animal, &desired_name) - .await; - - let name = match session_name { - Ok(session_name) => session_name, - Err(e) => { - return HttpError::new( - format!("Failed to store server config: {}", e), - StatusCode::INTERNAL_SERVER_ERROR, - ) - .into_response(); - } - }; - - (StatusCode::OK, name).into_response() + handle_session_upsert(state, upsert_req, NameKind::Animal).await } #[worker::send] async fn linkup_preview_handler( State(state): State, Json(upsert_req): Json, +) -> impl IntoResponse { + handle_session_upsert(state, upsert_req, NameKind::SixChar).await +} + +// TODO(augustoccesar)[2026-04-13]: This methods now exists because both the endpoints to +// create a preview session and a local session are exactly the same with the only +// difference being on the name generator kind. +// We should probably deprecate them as separate endpoints and create a new one that +// can take the name generator as part of the request. +async fn handle_session_upsert( + state: LinkupState, + req: UpsertSessionRequest, + name_kind: NameKind, ) -> impl IntoResponse { let store = CfWorkerStringStore::new(state.sessions_kv.clone()); let sessions = SessionAllocator::new(&store); - let desired_name = match &upsert_req { + let desired_name = match &req { UpsertSessionRequest::Named { desired_name, .. } => desired_name.clone(), UpsertSessionRequest::Unnamed { .. } => String::new(), }; - let server_conf: Session = match upsert_req.try_into() { + let session: Session = match req.try_into() { Ok(conf) => conf, Err(e) => { return HttpError::new( @@ -261,7 +240,7 @@ async fn linkup_preview_handler( }; let session_name = sessions - .store_session(server_conf, NameKind::SixChar, &desired_name) + .store_session(session, name_kind, &desired_name) .await; let name = match session_name { From bd209c5f209ff7b73a059c105ca6f2dcc8b66ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Augusto=20C=C3=A9sar?= Date: Mon, 13 Apr 2026 08:08:03 +0200 Subject: [PATCH 4/4] fix: test import --- linkup/src/session_allocator.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linkup/src/session_allocator.rs b/linkup/src/session_allocator.rs index abff21a2..1bd6c740 100644 --- a/linkup/src/session_allocator.rs +++ b/linkup/src/session_allocator.rs @@ -161,7 +161,7 @@ impl<'a, S: StringStore> SessionAllocator<'a, S> { #[cfg(test)] mod tests { use super::*; - use crate::{CreatePreviewRequest, MemoryStringStore}; + use crate::{MemoryStringStore, UpsertSessionRequest}; #[tokio::test] async fn identical_preview_requests_reuse_same_name() { @@ -195,7 +195,7 @@ mod tests { .to_string(); let first_session = - Session::try_from(serde_json::from_str::(&request_json).unwrap()) + Session::try_from(serde_json::from_str::(&request_json).unwrap()) .unwrap(); let mut second_session = first_session.clone();