@@ -23,6 +23,8 @@ struct LemonadeNexusClient::Impl {
2323 Identity identity;
2424 std::string session_token;
2525 std::string node_id;
26+ std::string server_private_fqdn; // e.g. "private.<id>.<region>.seip.lemonade-nexus.io"
27+ uint16_t private_port{9101 };
2628 mutable std::mutex mutex;
2729
2830 // Server pool with health state
@@ -221,6 +223,65 @@ struct LemonadeNexusClient::Impl {
221223 return std::nullopt ;
222224 }
223225
226+ // --- Private API (HTTPS over WG tunnel via private FQDN) ---
227+
228+ std::string private_base_url () const {
229+ if (!server_private_fqdn.empty ()) {
230+ return " https://" + server_private_fqdn + " :" + std::to_string (private_port);
231+ }
232+ return current_base_url ();
233+ }
234+
235+ std::optional<json> private_http_get (const std::string& path, int & status_out) {
236+ if (server_private_fqdn.empty ()) {
237+ return http_get (path, status_out);
238+ }
239+ try {
240+ httplib::Client cli (private_base_url ());
241+ cli.set_connection_timeout (config.connect_timeout_sec );
242+ cli.set_read_timeout (config.read_timeout_sec );
243+ cli.enable_server_certificate_verification (true );
244+ auto res = cli.Get (path, auth_headers ());
245+ if (!res) {
246+ spdlog::debug (" [LemonadeNexusClient] private GET {} failed, falling back" , path);
247+ return http_get (path, status_out);
248+ }
249+ status_out = res->status ;
250+ if (res->status < 200 || res->status >= 300 ) {
251+ try { return json::parse (res->body ); } catch (...) { return std::nullopt ; }
252+ }
253+ return json::parse (res->body );
254+ } catch (...) {
255+ return http_get (path, status_out);
256+ }
257+ }
258+
259+ std::optional<json> private_http_post (const std::string& path, const json& body, int & status_out) {
260+ if (server_private_fqdn.empty ()) {
261+ return http_post (path, body, status_out);
262+ }
263+ try {
264+ httplib::Client cli (private_base_url ());
265+ cli.set_connection_timeout (config.connect_timeout_sec );
266+ cli.set_read_timeout (config.read_timeout_sec );
267+ cli.enable_server_certificate_verification (true );
268+ auto res = cli.Post (path, auth_headers (), body.dump (), " application/json" );
269+ if (!res) {
270+ spdlog::debug (" [LemonadeNexusClient] private POST {} failed, falling back" , path);
271+ return http_post (path, body, status_out);
272+ }
273+ status_out = res->status ;
274+ if (res->status < 200 || res->status >= 300 ) {
275+ try { return json::parse (res->body ); } catch (...) {
276+ json err; err[" error" ] = " HTTP " + std::to_string (res->status ); return err;
277+ }
278+ }
279+ return json::parse (res->body );
280+ } catch (...) {
281+ return http_post (path, body, status_out);
282+ }
283+ }
284+
224285 // Discover additional servers via /api/servers
225286 void discover_servers () {
226287 if (has_discovered) return ;
@@ -767,7 +828,7 @@ Result<AuthResponse> LemonadeNexusClient::register_passkey_credential(const std:
767828Result<TreeNode> LemonadeNexusClient::get_tree_node (const std::string& node_id) {
768829 Result<TreeNode> result;
769830 int status = 0 ;
770- auto resp = impl_->http_get (" /api/tree/node/" + node_id, status);
831+ auto resp = impl_->private_http_get (" /api/tree/node/" + node_id, status);
771832 result.http_status = status;
772833
773834 if (!resp) {
@@ -798,7 +859,7 @@ Result<DeltaResult> LemonadeNexusClient::submit_delta(const TreeDelta& delta) {
798859 json body = signed_delta;
799860
800861 int status = 0 ;
801- auto resp = impl_->http_post (" /api/tree/delta" , body, status);
862+ auto resp = impl_->private_http_post (" /api/tree/delta" , body, status);
802863 result.http_status = status;
803864
804865 if (!resp) {
@@ -830,7 +891,7 @@ Result<DeltaResult> LemonadeNexusClient::create_child_node(const std::string& pa
830891 if (!child.hostname .empty ()) body[" hostname" ] = child.hostname ;
831892
832893 int status = 0 ;
833- auto resp = impl_->http_post (" /api/tree/node" , body, status);
894+ auto resp = impl_->private_http_post (" /api/tree/node" , body, status);
834895 result.http_status = status;
835896
836897 if (!resp) {
@@ -851,7 +912,7 @@ Result<DeltaResult> LemonadeNexusClient::update_node(const std::string& node_id,
851912 Result<DeltaResult> result;
852913
853914 int status = 0 ;
854- auto resp = impl_->http_post (" /api/tree/node/update/" + node_id, updates, status);
915+ auto resp = impl_->private_http_post (" /api/tree/node/update/" + node_id, updates, status);
855916 result.http_status = status;
856917
857918 if (!resp) {
@@ -872,7 +933,7 @@ Result<DeltaResult> LemonadeNexusClient::delete_node(const std::string& node_id)
872933
873934 int status = 0 ;
874935 json body = json::object (); // empty body
875- auto resp = impl_->http_post (" /api/tree/node/delete/" + node_id, body, status);
936+ auto resp = impl_->private_http_post (" /api/tree/node/delete/" + node_id, body, status);
876937 result.http_status = status;
877938
878939 if (!resp) {
@@ -890,7 +951,7 @@ Result<DeltaResult> LemonadeNexusClient::delete_node(const std::string& node_id)
890951Result<std::vector<TreeNode>> LemonadeNexusClient::get_children (const std::string& parent_id) {
891952 Result<std::vector<TreeNode>> result;
892953 int status = 0 ;
893- auto resp = impl_->http_get (" /api/tree/children/" + parent_id, status);
954+ auto resp = impl_->private_http_get (" /api/tree/children/" + parent_id, status);
894955 result.http_status = status;
895956
896957 if (!resp) {
@@ -930,7 +991,7 @@ Result<AllocationResponse> LemonadeNexusClient::allocate_ip(const AllocationRequ
930991 }
931992
932993 int status = 0 ;
933- auto resp = impl_->http_post (" /api/ipam/allocate" , body, status);
994+ auto resp = impl_->private_http_post (" /api/ipam/allocate" , body, status);
934995 result.http_status = status;
935996
936997 if (!resp) {
@@ -964,7 +1025,7 @@ Result<std::vector<RelayNodeInfo>> LemonadeNexusClient::list_relays() {
9641025 Result<std::vector<RelayNodeInfo>> result;
9651026
9661027 int status = 0 ;
967- auto resp = impl_->http_get (" /api/relay/list" , status);
1028+ auto resp = impl_->private_http_get (" /api/relay/list" , status);
9681029 result.http_status = status;
9691030
9701031 if (!resp) {
@@ -1001,7 +1062,7 @@ Result<RelayTicket> LemonadeNexusClient::request_relay_ticket(const std::string&
10011062 body[" relay_id" ] = relay_id;
10021063
10031064 int status = 0 ;
1004- auto resp = impl_->http_post (" /api/relay/ticket" , body, status);
1065+ auto resp = impl_->private_http_post (" /api/relay/ticket" , body, status);
10051066 result.http_status = status;
10061067
10071068 if (!resp) {
@@ -1032,7 +1093,7 @@ Result<RelayRegisterResult> LemonadeNexusClient::register_relay(const RelayRegis
10321093 body[" supports_relay" ] = reg.supports_relay ;
10331094
10341095 int status = 0 ;
1035- auto resp = impl_->http_post (" /api/relay/register" , body, status);
1096+ auto resp = impl_->private_http_post (" /api/relay/register" , body, status);
10361097 result.http_status = status;
10371098
10381099 if (!resp) {
@@ -1058,7 +1119,7 @@ Result<CertStatus> LemonadeNexusClient::get_cert_status(const std::string& domai
10581119 Result<CertStatus> result;
10591120
10601121 int status = 0 ;
1061- auto resp = impl_->http_get (" /api/certs/" + domain, status);
1122+ auto resp = impl_->private_http_get (" /api/certs/" + domain, status);
10621123 result.http_status = status;
10631124
10641125 if (!resp) {
@@ -1340,6 +1401,15 @@ Result<JoinResult> LemonadeNexusClient::join_network(const std::string& username
13401401 {
13411402 std::lock_guard lock (impl_->mutex );
13421403 impl_->node_id = node_id;
1404+ // Store server private FQDN for HTTPS over WG tunnel
1405+ auto srv_priv_fqdn = resp->value (" server_private_fqdn" , std::string{});
1406+ if (!srv_priv_fqdn.empty ()) {
1407+ impl_->server_private_fqdn = srv_priv_fqdn;
1408+ impl_->private_port = static_cast <uint16_t >(
1409+ resp->value (" private_api_port" , 9101 ));
1410+ spdlog::info (" [LemonadeNexusClient] private API via HTTPS: {}:{}" ,
1411+ srv_priv_fqdn, impl_->private_port );
1412+ }
13431413 }
13441414
13451415 // Step 4: Configure the WireGuard tunnel (bring up if we have a tunnel IP)
@@ -1769,7 +1839,7 @@ void LemonadeNexusClient::set_mesh_callback(MeshStateCallback cb) {
17691839
17701840Result<std::vector<MeshPeer>> LemonadeNexusClient::fetch_mesh_peers (const std::string& nid) {
17711841 int status = 0 ;
1772- auto resp = impl_->http_get (" /api/mesh/peers/" + nid, status);
1842+ auto resp = impl_->private_http_get (" /api/mesh/peers/" + nid, status);
17731843 if (!resp) {
17741844 return {false , {}, status, " mesh peers request failed" };
17751845 }
@@ -1834,7 +1904,7 @@ StatusResult LemonadeNexusClient::mesh_heartbeat(const std::string& nid,
18341904 body[" endpoint" ] = endpoint;
18351905
18361906 int status = 0 ;
1837- auto resp = impl_->http_post (" /api/mesh/heartbeat" , body, status);
1907+ auto resp = impl_->private_http_post (" /api/mesh/heartbeat" , body, status);
18381908 if (!resp) {
18391909 return {false , {}, status, " heartbeat request failed" };
18401910 }
0 commit comments