livedesk 0.1.445 → 0.1.446

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@ import { formatClientVersionBanner, readClientPackageVersion } from './client-ve
13
13
  import { createClientRuntimeServer } from '../src/runtime/client-runtime-server.js';
14
14
  import {
15
15
  createWindowsProcessTreeTracker,
16
- drainOwnedWindowsAgentTree,
16
+ drainOwnedWindowsAgentTreeUntilStopped,
17
17
  drainOwnedUnixAgentTree,
18
18
  installAgentTerminationHandlers,
19
19
  signalAgentTree
@@ -33,6 +33,8 @@ const nodeAgentPath = join(__dirname, 'livedesk-client-node.js');
33
33
  const FAST_PREFLIGHT_TIMEOUT_MS = 5000;
34
34
  const DEFAULT_MANAGER = '127.0.0.1:5197';
35
35
  const DEFAULT_HUB_CLIENT_PORT = 5197;
36
+ const DEFAULT_RELAY_HOST = 'www.gnsoftech.com';
37
+ const DEFAULT_RELAY_PORT = 5199;
36
38
  const DEFAULT_AUTH_CALLBACK_HOST = '127.0.0.1';
37
39
  const DEFAULT_AUTH_CALLBACK_PORT = 5179;
38
40
  const ENDPOINT_PROBE_TIMEOUT_MS = 900;
@@ -81,7 +83,7 @@ function requestActiveAgentStop(signal = 'SIGTERM') {
81
83
  // PID+CreationDate tree contract as unexpected exit and terminal
82
84
  // shutdown. Store the shared proof promise so no replacement can race.
83
85
  if (!child.livedeskTreeStopPromise) {
84
- child.livedeskTreeStopPromise = drainOwnedWindowsAgentTree(child);
86
+ child.livedeskTreeStopPromise = drainOwnedWindowsAgentTreeUntilStopped(child);
85
87
  }
86
88
  return true;
87
89
  }
@@ -213,9 +215,12 @@ Default flow:
213
215
  Options:
214
216
  --slot <number> Screen wall slot number for this computer.
215
217
  --name <name> Friendly device name. Default: OS hostname.
216
- --engine auto|fast|csharp|node Select agent engine. Default: auto.
217
- --fast Force C# RemoteFast frame.binary engine.
218
- --node Force the legacy Node agent.
218
+ --engine auto|fast|csharp|node Select agent engine. Default: auto.
219
+ --fast Force C# RemoteFast frame.binary engine.
220
+ --node Force the legacy Node agent.
221
+ --transport auto|tcp|ws|udp-p2p
222
+ Direct-first connection policy. Default: auto.
223
+ --relay <host:port> Encrypted fallback relay. Default: www.gnsoftech.com:5199.
219
224
  --control Enable RemoteFast keyboard/mouse control.
220
225
  --no-control Disable RemoteFast keyboard/mouse control.
221
226
  --thumbnail Enable thumbnail capture when supported.
@@ -247,7 +252,7 @@ function isTruthy(value) {
247
252
  return /^(1|true|yes|on)$/i.test(String(value || '').trim());
248
253
  }
249
254
 
250
- function normalizeEngine(value) {
255
+ function normalizeEngine(value) {
251
256
  const engine = String(value || 'auto').trim().toLowerCase();
252
257
  if (engine === 'c#' || engine === 'csharp' || engine === 'fast') {
253
258
  return 'fast';
@@ -255,11 +260,54 @@ function normalizeEngine(value) {
255
260
  if (engine === 'js' || engine === 'node' || engine === 'legacy') {
256
261
  return 'node';
257
262
  }
258
- return 'auto';
259
- }
260
-
261
- function parseLauncherArgs(argv) {
262
- const forwarded = [];
263
+ return 'auto';
264
+ }
265
+
266
+ function normalizeClientTransport(value) {
267
+ const transport = String(value || 'auto').trim().toLowerCase();
268
+ if (transport === 'legacy') return 'tcp';
269
+ if (transport === 'wss') return 'ws';
270
+ if (transport === 'udp-relay') return 'udp-p2p';
271
+ return ['auto', 'tcp', 'ws', 'udp-p2p'].includes(transport)
272
+ ? transport
273
+ : 'tcp';
274
+ }
275
+
276
+ export function parseClientTransportOption(value) {
277
+ const transport = String(value || '').trim().toLowerCase();
278
+ if (!['auto', 'tcp', 'legacy', 'ws', 'wss', 'udp-p2p', 'udp-relay'].includes(transport)) {
279
+ throw new Error(`Unsupported LiveDesk Client transport: ${value || '(missing)'}`);
280
+ }
281
+ return normalizeClientTransport(transport);
282
+ }
283
+
284
+ function transportAllowsRelay(transport) {
285
+ return transport === 'auto' || transport === 'udp-p2p';
286
+ }
287
+
288
+ export function resolveClientRelayEndpoint(env = process.env) {
289
+ const explicit = String(
290
+ env.LIVEDESK_CLIENT_RELAY
291
+ || env.LIVEDESK_RELAY_ENDPOINT
292
+ || ''
293
+ ).trim().replace(/^tcp:\/\//i, '');
294
+ if (parseManagerEndpoint(explicit)) {
295
+ return explicit;
296
+ }
297
+ const host = String(
298
+ env.LIVEDESK_RELAY_HOST
299
+ || env.LIVEDESK_UDP_RENDEZVOUS_HOST
300
+ || DEFAULT_RELAY_HOST
301
+ ).trim();
302
+ const port = normalizePort(
303
+ env.LIVEDESK_RELAY_PORT
304
+ || env.LIVEDESK_UDP_RENDEZVOUS_PORT
305
+ ) || DEFAULT_RELAY_PORT;
306
+ return host ? `${host}:${port}` : '';
307
+ }
308
+
309
+ export function parseLauncherArgs(argv) {
310
+ const forwarded = [];
263
311
  let engine = normalizeEngine(process.env.LIVEDESK_CLIENT_ENGINE || process.env.MINDEXEC_REMOTE_ENGINE);
264
312
  let help = false;
265
313
  let loginRequested = false;
@@ -267,7 +315,18 @@ function parseLauncherArgs(argv) {
267
315
  let logout = false;
268
316
  let version = false;
269
317
  let manager = process.env.LIVEDESK_CLIENT_MANAGER || process.env.MINDEXEC_REMOTE_MANAGER || '';
270
- let pair = process.env.LIVEDESK_CLIENT_PAIR_TOKEN || process.env.MINDEXEC_REMOTE_PAIR_TOKEN || '';
318
+ let pair = process.env.LIVEDESK_CLIENT_PAIR_TOKEN || process.env.MINDEXEC_REMOTE_PAIR_TOKEN || '';
319
+ let transport = normalizeClientTransport(
320
+ process.env.LIVEDESK_CLIENT_TRANSPORT || process.env.MINDEXEC_REMOTE_TRANSPORT);
321
+ let relay = resolveClientRelayEndpoint();
322
+ const requireFast = isTruthy(process.env.LIVEDESK_CLIENT_REQUIRE_FAST);
323
+ let relayExplicit = Boolean(String(
324
+ process.env.LIVEDESK_CLIENT_RELAY
325
+ || process.env.LIVEDESK_RELAY_ENDPOINT
326
+ || process.env.LIVEDESK_RELAY_HOST
327
+ || process.env.LIVEDESK_UDP_RENDEZVOUS_HOST
328
+ || ''
329
+ ).trim());
271
330
  let deviceId = String(process.env.LIVEDESK_DEVICE_ID || '').trim();
272
331
  let slot = process.env.LIVEDESK_CLIENT_SLOT || process.env.MINDEXEC_REMOTE_SLOT || '';
273
332
  let authPort = normalizePort(process.env.LIVEDESK_CLIENT_RUNTIME_PORT || process.env.LIVEDESK_CLIENT_AUTH_PORT) || DEFAULT_AUTH_CALLBACK_PORT;
@@ -347,13 +406,28 @@ function parseLauncherArgs(argv) {
347
406
  index += 1;
348
407
  continue;
349
408
  }
350
- if (arg === '--pair') {
351
- pair = argv[index + 1] || pair;
352
- forwarded.push(arg, argv[index + 1]);
353
- index += 1;
354
- continue;
355
- }
356
- if (arg === '--device-id') {
409
+ if (arg === '--pair') {
410
+ pair = argv[index + 1] || pair;
411
+ forwarded.push(arg, argv[index + 1]);
412
+ index += 1;
413
+ continue;
414
+ }
415
+ if (arg === '--transport') {
416
+ transport = parseClientTransportOption(argv[index + 1]);
417
+ index += 1;
418
+ continue;
419
+ }
420
+ if (arg === '--relay') {
421
+ const candidate = String(argv[index + 1] || '').trim().replace(/^tcp:\/\//i, '');
422
+ if (!parseManagerEndpoint(candidate)) {
423
+ throw new Error(`Invalid LiveDesk relay endpoint: ${argv[index + 1] || '(missing)'}`);
424
+ }
425
+ relay = candidate;
426
+ relayExplicit = true;
427
+ index += 1;
428
+ continue;
429
+ }
430
+ if (arg === '--device-id') {
357
431
  deviceId = argv[index + 1] || deviceId;
358
432
  forwarded.push(arg, argv[index + 1]);
359
433
  index += 1;
@@ -393,9 +467,13 @@ function parseLauncherArgs(argv) {
393
467
  loginDisabled,
394
468
  logout,
395
469
  version,
396
- manager,
397
- pair,
398
- deviceId,
470
+ manager,
471
+ pair,
472
+ transport,
473
+ relay,
474
+ relayExplicit,
475
+ requireFast,
476
+ deviceId,
399
477
  slot,
400
478
  authPort,
401
479
  nodeOnlyFeature,
@@ -573,13 +651,19 @@ function buildStartupClientArgs(parsed) {
573
651
  if (slot) {
574
652
  args.push(slot);
575
653
  }
576
- if (parsed.engine === 'fast') {
577
- args.push('--fast');
578
- } else if (parsed.engine === 'node') {
579
- args.push('--node');
580
- }
581
-
582
- const forwarded = Array.isArray(parsed.forwarded) ? parsed.forwarded : [];
654
+ if (parsed.engine === 'fast') {
655
+ args.push('--fast');
656
+ } else if (parsed.engine === 'node') {
657
+ args.push('--node');
658
+ }
659
+ if (parsed.transport && parsed.transport !== 'auto') {
660
+ args.push('--transport', parsed.transport);
661
+ }
662
+ if (parsed.relayExplicit && parsed.relay) {
663
+ args.push('--relay', parsed.relay);
664
+ }
665
+
666
+ const forwarded = Array.isArray(parsed.forwarded) ? parsed.forwarded : [];
583
667
  for (let index = 0; index < forwarded.length; index += 1) {
584
668
  const arg = forwarded[index];
585
669
  if (!arg || arg === 'connect') {
@@ -2471,7 +2555,7 @@ function readRequestBody(req, maxBytes = 4096) {
2471
2555
  });
2472
2556
  }
2473
2557
 
2474
- async function resolveManagerFromPin(supabase, pin) {
2558
+ async function resolveManagerFromPin(supabase, pin, options = {}) {
2475
2559
  const normalizedPin = normalizePairingPin(pin);
2476
2560
  if (!normalizedPin) {
2477
2561
  throw new Error('Enter a 6-digit LiveDesk PIN.');
@@ -2500,16 +2584,20 @@ async function resolveManagerFromPin(supabase, pin) {
2500
2584
  if (endpointCandidates.length === 0) {
2501
2585
  throw new Error('The PIN matched a Hub record without a reachable address.');
2502
2586
  }
2503
- const manager = await chooseReachableEndpoint(endpointCandidates, { requireReachable: true });
2504
- if (!manager) {
2505
- throw new Error(`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`);
2506
- }
2507
-
2587
+ const target = await chooseManagerConnectionTarget(endpointCandidates, {
2588
+ allowRelayFallback: options.allowRelayFallback === true
2589
+ });
2590
+ if (!target) {
2591
+ throw new Error(`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`);
2592
+ }
2593
+
2508
2594
  const resolved = {
2509
- manager,
2595
+ manager: target.manager,
2510
2596
  pair: result.pair_token,
2511
2597
  hubDeviceId: String(result.node_id || '').trim(),
2512
2598
  endpointCandidates,
2599
+ directReachable: target.directReachable,
2600
+ connectionTransport: target.connectionTransport,
2513
2601
  discoverySource: 'supabase'
2514
2602
  };
2515
2603
  writeCachedHubTarget(cacheOwnerKey, resolved);
@@ -2528,7 +2616,9 @@ async function waitForManagerFromSavedPin(supabase, pin, options = {}) {
2528
2616
  }
2529
2617
  attempts += 1;
2530
2618
  try {
2531
- return await resolveManagerFromPin(supabase, pin);
2619
+ return await resolveManagerFromPin(supabase, pin, {
2620
+ allowRelayFallback: options.allowRelayFallback === true
2621
+ });
2532
2622
  } catch (err) {
2533
2623
  if (shouldStop()) {
2534
2624
  return null;
@@ -2912,7 +3002,9 @@ async function startConnectionChoiceServer(supabase, options = {}) {
2912
3002
  pendingStartup = normalizeStartupChoice(startupValue, pendingStartup);
2913
3003
  pin = normalizePairingPin(pin);
2914
3004
  try {
2915
- const resolved = await resolveManagerFromPin(supabase, pin);
3005
+ const resolved = await resolveManagerFromPin(supabase, pin, {
3006
+ allowRelayFallback: options.allowRelayFallback === true
3007
+ });
2916
3008
  writeSavedPin(pin);
2917
3009
  applyStartupPreference(pendingStartup, startupArgs);
2918
3010
  const choice = { type: 'pin', ...resolved };
@@ -3073,7 +3165,9 @@ async function chooseClientConnection(supabase, options = {}) {
3073
3165
  },
3074
3166
  resolvePin: pin => {
3075
3167
  if (!supabase) throw new Error('supabase-session-required');
3076
- return resolveManagerFromPin(supabase, pin);
3168
+ return resolveManagerFromPin(supabase, pin, {
3169
+ allowRelayFallback: options.allowRelayFallback === true
3170
+ });
3077
3171
  },
3078
3172
  changeRole: async (role, snapshot) => {
3079
3173
  if (!supabase) {
@@ -3169,9 +3263,10 @@ async function chooseClientConnection(supabase, options = {}) {
3169
3263
  deviceId: options.deviceId,
3170
3264
  slot: options.slot,
3171
3265
  startupArgs: options.startupArgs,
3172
- savedSession: options.savedSession,
3173
- savedPin: options.savedPin
3174
- });
3266
+ savedSession: options.savedSession,
3267
+ savedPin: options.savedPin,
3268
+ allowRelayFallback: options.allowRelayFallback === true
3269
+ });
3175
3270
  if (options.openBrowser !== false) {
3176
3271
  console.log('Opening LiveDesk connection page...');
3177
3272
  openBrowser(connectionPage.url);
@@ -3186,11 +3281,11 @@ function parseManagerEndpoint(value) {
3186
3281
  if (separator <= 0) {
3187
3282
  return null;
3188
3283
  }
3189
- const host = text.slice(0, separator).replace(/^\[/, '').replace(/\]$/, '').trim();
3190
- const port = Number(text.slice(separator + 1));
3191
- if (!host || !Number.isFinite(port)) {
3192
- return null;
3193
- }
3284
+ const host = text.slice(0, separator).replace(/^\[/, '').replace(/\]$/, '').trim();
3285
+ const port = Number(text.slice(separator + 1));
3286
+ if (!host || !Number.isInteger(port) || port < 1 || port > 65535) {
3287
+ return null;
3288
+ }
3194
3289
  return { host, port };
3195
3290
  }
3196
3291
 
@@ -3316,6 +3411,37 @@ export async function chooseReachableEndpoint(candidates, options = {}) {
3316
3411
  });
3317
3412
  }
3318
3413
 
3414
+ export async function chooseManagerConnectionTarget(candidates, options = {}) {
3415
+ const endpoints = [...new Set((Array.isArray(candidates) ? candidates : [])
3416
+ .map(value => String(value || '').trim())
3417
+ .filter(Boolean))];
3418
+ if (endpoints.length === 0) return null;
3419
+
3420
+ const manager = await chooseReachableEndpoint(endpoints, {
3421
+ requireReachable: true,
3422
+ probeEndpoint: options.probeEndpoint
3423
+ });
3424
+ if (manager) {
3425
+ return {
3426
+ manager,
3427
+ directReachable: true,
3428
+ connectionTransport: 'direct-tcp'
3429
+ };
3430
+ }
3431
+ if (options.allowRelayFallback !== true) {
3432
+ return null;
3433
+ }
3434
+ return {
3435
+ // The account/PIN response is the authority for this retry candidate.
3436
+ // RemoteFast gives it one bounded direct attempt, then uses the
3437
+ // pair-token-authenticated encrypted relay rather than keeping the
3438
+ // launcher in an indefinite TCP reachability loop.
3439
+ manager: endpoints[0],
3440
+ directReachable: false,
3441
+ connectionTransport: 'relay-fallback'
3442
+ };
3443
+ }
3444
+
3319
3445
  export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
3320
3446
  const cached = options.cachedTarget || readCachedHubTarget(ownerKey);
3321
3447
  if (!cached) return null;
@@ -3411,15 +3537,19 @@ async function resolveManagerFromSupabase(supabase, options = {}) {
3411
3537
  if (endpointCandidates.length === 0) {
3412
3538
  throw new Error('The LiveDesk Hub record does not contain a reachable local address.');
3413
3539
  }
3414
- const manager = await chooseReachableEndpoint(endpointCandidates, { requireReachable: options.requireReachable === true });
3415
- if (!manager) {
3416
- throw new Error(`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`);
3417
- }
3418
- return {
3419
- manager,
3420
- pair: data.pair_token,
3421
- hubDeviceId: String(data.node_id || '').trim(),
3422
- endpointCandidates
3540
+ const target = await chooseManagerConnectionTarget(endpointCandidates, {
3541
+ allowRelayFallback: options.allowRelayFallback === true
3542
+ });
3543
+ if (!target) {
3544
+ throw new Error(`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`);
3545
+ }
3546
+ return {
3547
+ manager: target.manager,
3548
+ pair: data.pair_token,
3549
+ hubDeviceId: String(data.node_id || '').trim(),
3550
+ endpointCandidates,
3551
+ directReachable: target.directReachable,
3552
+ connectionTransport: target.connectionTransport
3423
3553
  };
3424
3554
  }
3425
3555
 
@@ -3462,7 +3592,9 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
3462
3592
  }
3463
3593
  attempts += 1;
3464
3594
  try {
3465
- return await resolveManagerFromSupabase(supabase, { requireReachable: true });
3595
+ return await resolveManagerFromSupabase(supabase, {
3596
+ allowRelayFallback: options.allowRelayFallback === true
3597
+ });
3466
3598
  } catch (err) {
3467
3599
  const message = formatDiscoveryError(err);
3468
3600
  if (message !== lastMessage || attempts === 1 || attempts % 6 === 0) {
@@ -3515,6 +3647,9 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3515
3647
  let pair = parsed.pair;
3516
3648
  let connectionPage = null;
3517
3649
  let discoverySource = '';
3650
+ let connectionTransport = '';
3651
+ let directReachable = false;
3652
+ const allowRelayFallback = transportAllowsRelay(parsed.transport);
3518
3653
 
3519
3654
  if (shouldLogin) {
3520
3655
  const supabase = await createSupabaseClient();
@@ -3531,7 +3666,9 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3531
3666
  if ((parsed.startupRun || existingConnectionPage) && savedSession?.access_token) {
3532
3667
  choice = { type: 'google', session: savedSession };
3533
3668
  } else if ((parsed.startupRun || existingConnectionPage) && savedPin) {
3534
- const resolved = await waitForManagerFromSavedPin(supabase, savedPin);
3669
+ const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
3670
+ allowRelayFallback
3671
+ });
3535
3672
  choice = { type: 'pin', ...resolved };
3536
3673
  } else if (existingConnectionPage && previousChoice) {
3537
3674
  choice = previousChoice;
@@ -3543,10 +3680,11 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3543
3680
  deviceId: parsed.deviceId,
3544
3681
  engine: parsed.engine,
3545
3682
  slot: parsed.slot,
3546
- startupArgs,
3547
- savedSession,
3548
- savedPin,
3549
- openBrowser: parsed.openBrowserOnStart
3683
+ startupArgs,
3684
+ savedSession,
3685
+ savedPin,
3686
+ allowRelayFallback,
3687
+ openBrowser: parsed.openBrowserOnStart
3550
3688
  });
3551
3689
  }
3552
3690
  connectionPage = existingConnectionPage || choice.connectionPage || null;
@@ -3554,19 +3692,27 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3554
3692
  manager = choice.manager;
3555
3693
  pair = choice.pair;
3556
3694
  discoverySource = choice.discoverySource || 'supabase';
3695
+ connectionTransport = choice.connectionTransport || 'direct-tcp';
3696
+ directReachable = choice.directReachable !== false;
3557
3697
  connectionPage?.update({
3558
3698
  endpointCandidates: choice.endpointCandidates || [],
3559
3699
  manager,
3560
3700
  assignedHubId: choice.hubDeviceId || '',
3561
- message: `Connected to LiveDesk Hub at ${manager}.`
3562
- });
3563
- console.log('Connected by LiveDesk PIN.');
3701
+ message: connectionTransport === 'relay-fallback'
3702
+ ? `LiveDesk Hub found at ${manager}; starting encrypted relay fallback.`
3703
+ : `Connected to LiveDesk Hub at ${manager}.`
3704
+ });
3705
+ console.log(connectionTransport === 'relay-fallback'
3706
+ ? 'LiveDesk PIN accepted. Direct TCP is unavailable; starting encrypted relay fallback.'
3707
+ : 'Connected by LiveDesk PIN.');
3564
3708
  } else {
3565
3709
  const cacheOwnerKey = hubCacheOwnerForSession(choice.session);
3566
3710
  let resolved = await resolveManagerFromCachedTarget(cacheOwnerKey);
3567
3711
  let session = choice.session;
3568
3712
  discoverySource = resolved ? 'cache' : 'supabase';
3569
3713
  if (resolved) {
3714
+ connectionTransport = resolved.connectionTransport || 'direct-tcp';
3715
+ directReachable = resolved.directReachable !== false;
3570
3716
  writeSavedSessionToFile(session);
3571
3717
  console.log(`Found cached LiveDesk Hub at ${resolved.manager}; skipped registry discovery.`);
3572
3718
  } else {
@@ -3587,6 +3733,7 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3587
3733
  console.log(`Signed in to LiveDesk${email}.`);
3588
3734
  if (!resolved) {
3589
3735
  resolved = await waitForManagerFromSupabase(supabase, {
3736
+ allowRelayFallback,
3590
3737
  shouldStop: () => Boolean(roleRestartRequest?.role)
3591
3738
  });
3592
3739
  }
@@ -3615,15 +3762,21 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3615
3762
  }
3616
3763
  }
3617
3764
  manager = resolved.manager;
3618
- pair = resolved.pair;
3765
+ pair = resolved.pair;
3766
+ connectionTransport = resolved.connectionTransport || 'direct-tcp';
3767
+ directReachable = resolved.directReachable !== false;
3619
3768
  connectionPage?.update({
3620
3769
  endpointCandidates: resolved.endpointCandidates || [],
3621
3770
  manager,
3622
3771
  assignedHubId: resolved.hubDeviceId || '',
3623
- message: `Connected to LiveDesk Hub at ${manager}.`
3624
- });
3625
- }
3626
- console.log(`Found LiveDesk Hub at ${manager}.`);
3772
+ message: connectionTransport === 'relay-fallback'
3773
+ ? `LiveDesk Hub found at ${manager}; starting encrypted relay fallback.`
3774
+ : `Connected to LiveDesk Hub at ${manager}.`
3775
+ });
3776
+ }
3777
+ console.log(connectionTransport === 'relay-fallback'
3778
+ ? `Found LiveDesk Hub record at ${manager}; direct TCP did not answer, so RemoteFast will use the encrypted rendezvous relay.`
3779
+ : `Found LiveDesk Hub at ${manager}.`);
3627
3780
  }
3628
3781
  // Exact-package updates preserve the already paired Hub endpoint and pass
3629
3782
  // --no-login so the replacement cannot block on browser auth. The unified
@@ -3669,12 +3822,16 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3669
3822
  forwarded = appendForwardedFlag(forwarded, '--exit-on-invalid-pair');
3670
3823
  }
3671
3824
  return {
3672
- ...parsed,
3673
- manager,
3674
- pair,
3675
- slot,
3825
+ ...parsed,
3826
+ manager,
3827
+ pair,
3828
+ transport: parsed.transport,
3829
+ relay: parsed.relay,
3830
+ slot,
3676
3831
  connectionPage,
3677
3832
  forwarded,
3833
+ connectionTransport: connectionTransport || 'direct-tcp',
3834
+ directReachable: connectionTransport === 'relay-fallback' ? false : (directReachable || !shouldLogin),
3678
3835
  discoverySource,
3679
3836
  rediscoverOnDisconnect: shouldLogin,
3680
3837
  rediscoverOnInvalidPair: shouldLogin
@@ -3894,7 +4051,9 @@ function buildClientAgentEnvironment(prepared) {
3894
4051
  env.LIVEDESK_CLIENT_SLOT = String(prepared?.slot || env.LIVEDESK_CLIENT_SLOT || '');
3895
4052
  env.LIVEDESK_DEVICE_ID = String(prepared?.deviceId || env.LIVEDESK_DEVICE_ID || '');
3896
4053
  env.LIVEDESK_CLIENT_ENGINE = String(prepared?.engine || env.LIVEDESK_CLIENT_ENGINE || 'auto');
3897
- env.LIVEDESK_CLIENT_TRANSPORT = String(env.LIVEDESK_CLIENT_TRANSPORT || 'auto');
4054
+ env.LIVEDESK_CLIENT_TRANSPORT = String(prepared?.transport || env.LIVEDESK_CLIENT_TRANSPORT || 'auto');
4055
+ env.LIVEDESK_CLIENT_RELAY = String(prepared?.relay || env.LIVEDESK_CLIENT_RELAY || '');
4056
+ env.LIVEDESK_CLIENT_REQUIRE_FAST = requiresFastTransport(prepared) ? '1' : '0';
3898
4057
  env.LIVEDESK_UDP_ENABLED = String(env.LIVEDESK_UDP_ENABLED || '1');
3899
4058
  env.LIVEDESK_CLIENT_UPDATE_STATE_PATH = CLIENT_UPDATE_STATE_PATH;
3900
4059
  env.LIVEDESK_CLIENT_UPDATE_ARGS_BASE64 = Buffer
@@ -4007,23 +4166,35 @@ function prepareFastExecutable(executable) {
4007
4166
  }
4008
4167
  }
4009
4168
 
4010
- function buildFastArgs(args, fakeThumbnail) {
4011
- const forwarded = [];
4012
- for (const arg of args) {
4013
- if (arg === '--fake-thumbnail') {
4014
- forwarded.push('--fake-frame');
4015
- continue;
4169
+ export function buildFastArgs(args, fakeThumbnail, transport = 'auto', relay = '') {
4170
+ const forwarded = [];
4171
+ for (let index = 0; index < args.length; index += 1) {
4172
+ const arg = args[index];
4173
+ if (arg === '--transport' || arg === '--relay') {
4174
+ index += 1;
4175
+ continue;
4176
+ }
4177
+ if (arg === '--fake-thumbnail') {
4178
+ forwarded.push('--fake-frame');
4179
+ continue;
4016
4180
  }
4017
4181
  if (arg === '--tasks' || arg === '--no-tasks' || arg === '--no-ai') {
4018
4182
  continue;
4019
4183
  }
4020
4184
  forwarded.push(arg);
4021
4185
  }
4022
- if (fakeThumbnail && !forwarded.includes('--fake-frame')) {
4023
- forwarded.push('--fake-frame');
4024
- }
4025
- return forwarded;
4026
- }
4186
+ if (fakeThumbnail && !forwarded.includes('--fake-frame')) {
4187
+ forwarded.push('--fake-frame');
4188
+ }
4189
+ let launchArgs = upsertForwardedOption(
4190
+ forwarded,
4191
+ '--transport',
4192
+ normalizeClientTransport(transport));
4193
+ if (transportAllowsRelay(normalizeClientTransport(transport))) {
4194
+ launchArgs = upsertForwardedOption(launchArgs, '--relay', relay);
4195
+ }
4196
+ return launchArgs;
4197
+ }
4027
4198
 
4028
4199
  function redactAgentArgs(args = []) {
4029
4200
  const redacted = [];
@@ -4169,7 +4340,7 @@ function spawnAgent(command, args, env = process.env, onStart = null) {
4169
4340
  try {
4170
4341
  if (process.platform === 'win32') {
4171
4342
  if (!child.livedeskTreeStopPromise) {
4172
- child.livedeskTreeStopPromise = drainOwnedWindowsAgentTree(child);
4343
+ child.livedeskTreeStopPromise = drainOwnedWindowsAgentTreeUntilStopped(child);
4173
4344
  }
4174
4345
  const windowsTreeStop = await child.livedeskTreeStopPromise;
4175
4346
  if (windowsTreeStop?.ok === false) {
@@ -4219,12 +4390,20 @@ function shouldUseFast(parsed) {
4219
4390
  return !parsed.nodeOnlyFeature;
4220
4391
  }
4221
4392
 
4222
- function shouldTryFast(parsed, runtime) {
4393
+ function shouldTryFast(parsed, runtime) {
4223
4394
  if (parsed.engine === 'fast') {
4224
4395
  return true;
4225
4396
  }
4226
4397
  return shouldUseFast(parsed) && !!runtime;
4227
- }
4398
+ }
4399
+
4400
+ export function requiresFastTransport(prepared = {}) {
4401
+ return prepared.requireFast === true
4402
+ || prepared.connectionTransport === 'relay-fallback'
4403
+ || prepared.transport === 'udp-p2p'
4404
+ || prepared.transport === 'ws'
4405
+ || prepared.relayExplicit === true;
4406
+ }
4228
4407
 
4229
4408
  export async function runClientRuntime(argv = process.argv.slice(2)) {
4230
4409
  startNetworkChangeMonitor();
@@ -4278,9 +4457,16 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
4278
4457
  await spawnUnifiedRoleRestart(roleRestartBeforeAgent.role);
4279
4458
  connectionPage?.close?.();
4280
4459
  process.exit(0);
4281
- }
4460
+ }
4282
4461
  const fastRuntime = getFastRuntime();
4283
- const useFast = shouldTryFast(prepared, fastRuntime);
4462
+ const fastRequired = requiresFastTransport(prepared);
4463
+ if (fastRequired && prepared.engine === 'node') {
4464
+ throw new Error(
4465
+ 'The legacy Node engine cannot use WebSocket, UDP P2P, or encrypted relay transport. '
4466
+ + 'Use --engine fast, or use --transport tcp while the Hub is directly reachable.'
4467
+ );
4468
+ }
4469
+ const useFast = fastRequired || shouldTryFast(prepared, fastRuntime);
4284
4470
  const agentLaunchStartedAt = Date.now();
4285
4471
  const updateAgentDashboard = (patch = {}) => {
4286
4472
  prepared.connectionPage?.update({
@@ -4294,11 +4480,15 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
4294
4480
  ? `LiveDesk client agent is running with ${patch.engine || 'agent'} mode.`
4295
4481
  : 'LiveDesk client agent is launching.'
4296
4482
  });
4297
- };
4298
- let result;
4299
- if (useFast) {
4300
- const fastArgs = buildFastArgs(prepared.forwarded, prepared.fakeThumbnail);
4301
- const fastLaunch = resolveFastLaunch(fastRuntime);
4483
+ };
4484
+ let result;
4485
+ if (useFast) {
4486
+ const fastArgs = buildFastArgs(
4487
+ prepared.forwarded,
4488
+ prepared.fakeThumbnail,
4489
+ prepared.transport,
4490
+ prepared.relay);
4491
+ const fastLaunch = resolveFastLaunch(fastRuntime);
4302
4492
  if (fastLaunch.ok) {
4303
4493
  const launchArgs = [...fastLaunch.argsPrefix, ...fastArgs];
4304
4494
  updateAgentDashboard({
@@ -4319,10 +4509,15 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
4319
4509
  state: 'running'
4320
4510
  });
4321
4511
  });
4322
- } else if (prepared.engine === 'fast') {
4323
- console.error(`C# RemoteFast is unavailable: ${fastLaunch.reason}. Use --engine node to run the legacy Node agent.`);
4324
- prepared.connectionPage?.close?.();
4325
- process.exit(2);
4512
+ } else if (prepared.engine === 'fast' || fastRequired) {
4513
+ console.error(
4514
+ `C# RemoteFast is unavailable: ${fastLaunch.reason}. `
4515
+ + (fastRequired
4516
+ ? 'The requested P2P, WebSocket, or encrypted relay transport requires the packaged RemoteFast runtime; the legacy Node agent was not started.'
4517
+ : 'Use --engine node to run the legacy Node agent.')
4518
+ );
4519
+ prepared.connectionPage?.close?.();
4520
+ process.exit(2);
4326
4521
  } else {
4327
4522
  console.warn(`C# RemoteFast is unavailable (${fastLaunch.reason}). Falling back to the Node remote agent.`);
4328
4523
  const launchArgs = [nodeAgentPath, ...prepared.forwarded];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.200",
3
+ "version": "0.1.201",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,10 +41,10 @@
41
41
  "ws": "^8.18.3"
42
42
  },
43
43
  "optionalDependencies": {
44
- "@livedesk/fast-linux-x64": "0.1.407",
45
- "@livedesk/fast-osx-arm64": "0.1.407",
46
- "@livedesk/fast-osx-x64": "0.1.407",
47
- "@livedesk/fast-win-x64": "0.1.407"
44
+ "@livedesk/fast-linux-x64": "0.1.409",
45
+ "@livedesk/fast-osx-arm64": "0.1.409",
46
+ "@livedesk/fast-osx-x64": "0.1.409",
47
+ "@livedesk/fast-win-x64": "0.1.409"
48
48
  },
49
49
  "publishConfig": {
50
50
  "access": "public"