@livedesk/client 0.1.200 → 0.1.202

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,11 +13,12 @@ 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
20
20
  } from '../src/runtime/agent-process-lifecycle.js';
21
+ import { writeWindowsOwnedProcessManifest } from '../src/runtime/windows-owned-process-manifest.js';
21
22
  import { createHubWakeListener } from '../src/runtime/hub-wake-listener.js';
22
23
  import {
23
24
  inspectLinuxVideoAcceleration,
@@ -33,6 +34,8 @@ const nodeAgentPath = join(__dirname, 'livedesk-client-node.js');
33
34
  const FAST_PREFLIGHT_TIMEOUT_MS = 5000;
34
35
  const DEFAULT_MANAGER = '127.0.0.1:5197';
35
36
  const DEFAULT_HUB_CLIENT_PORT = 5197;
37
+ const DEFAULT_RELAY_HOST = 'www.gnsoftech.com';
38
+ const DEFAULT_RELAY_PORT = 5199;
36
39
  const DEFAULT_AUTH_CALLBACK_HOST = '127.0.0.1';
37
40
  const DEFAULT_AUTH_CALLBACK_PORT = 5179;
38
41
  const ENDPOINT_PROBE_TIMEOUT_MS = 900;
@@ -71,7 +74,69 @@ let linuxVideoAccelerationStatus = null;
71
74
  let discoveryWakeController = new AbortController();
72
75
  let networkChangeMonitor = null;
73
76
  const sessionRefreshesInFlight = new WeakMap();
74
- installAgentTerminationHandlers({ getAgentProcess: () => activeAgentProcess });
77
+ const disposeAgentTerminationHandlers = installAgentTerminationHandlers({
78
+ getAgentProcess: () => activeAgentProcess
79
+ });
80
+
81
+ function readWindowsManifestOwnerFromEnvironment(env = process.env) {
82
+ if (process.platform !== 'win32') return null;
83
+ const rawOwner = {
84
+ pid: String(env.LIVEDESK_RUNTIME_OWNER_PID || '').trim(),
85
+ ownerToken: String(env.LIVEDESK_RUNTIME_OWNER_TOKEN || '').trim(),
86
+ ownerInstanceMarker: String(env.LIVEDESK_RUNTIME_OWNER_INSTANCE_MARKER || '').trim(),
87
+ ownerStartOrder: String(env.LIVEDESK_RUNTIME_OWNER_START_ORDER || '').trim()
88
+ };
89
+ const hasAnyOwnerField = Object.values(rawOwner).some(Boolean);
90
+ if (!hasAnyOwnerField) return null;
91
+ const owner = {
92
+ ...rawOwner,
93
+ pid: Number(rawOwner.pid)
94
+ };
95
+ if (owner.pid !== process.pid
96
+ || !/^[a-f0-9]{32}$/i.test(owner.ownerToken)
97
+ || !owner.ownerInstanceMarker.startsWith(`${process.pid}:`)
98
+ || !/^\d+$/.test(owner.ownerStartOrder)) {
99
+ console.warn(
100
+ '[LiveDesk Client] Windows process-manifest publication is disabled because '
101
+ + 'the unified launcher owner tuple is incomplete or does not name this exact process.'
102
+ );
103
+ return null;
104
+ }
105
+ return owner;
106
+ }
107
+
108
+ const WINDOWS_PROCESS_MANIFEST_OWNER = readWindowsManifestOwnerFromEnvironment();
109
+
110
+ function publishWindowsOwnedAgentRecords(records, {
111
+ rootRecord,
112
+ snapshot
113
+ } = {}) {
114
+ if (!WINDOWS_PROCESS_MANIFEST_OWNER || !rootRecord || records.length === 0) return;
115
+ const ownerRecord = (Array.isArray(snapshot) ? snapshot : []).find(
116
+ record => Number(record?.pid || 0) === WINDOWS_PROCESS_MANIFEST_OWNER.pid
117
+ );
118
+ const exactOwnerGeneration = ownerRecord
119
+ && ownerRecord.startOrder === WINDOWS_PROCESS_MANIFEST_OWNER.ownerStartOrder
120
+ && `${ownerRecord.pid}:${ownerRecord.startMarker}`
121
+ === WINDOWS_PROCESS_MANIFEST_OWNER.ownerInstanceMarker;
122
+ if (!exactOwnerGeneration) {
123
+ throw new Error(
124
+ 'The Windows process snapshot did not confirm the unified launcher owner generation.'
125
+ );
126
+ }
127
+ const publication = writeWindowsOwnedProcessManifest({
128
+ stateDir: UNIFIED_CLIENT_STATE_DIR,
129
+ owner: WINDOWS_PROCESS_MANIFEST_OWNER,
130
+ records
131
+ });
132
+ if (!publication.ok) {
133
+ throw publication.error || new Error('Windows owned-process manifest publication failed.');
134
+ }
135
+ }
136
+
137
+ export function requestClientRuntimeShutdown(signal = 'SIGTERM') {
138
+ disposeAgentTerminationHandlers.requestTermination(signal);
139
+ }
75
140
 
76
141
  function requestActiveAgentStop(signal = 'SIGTERM') {
77
142
  const child = activeAgentProcess;
@@ -81,7 +146,7 @@ function requestActiveAgentStop(signal = 'SIGTERM') {
81
146
  // PID+CreationDate tree contract as unexpected exit and terminal
82
147
  // shutdown. Store the shared proof promise so no replacement can race.
83
148
  if (!child.livedeskTreeStopPromise) {
84
- child.livedeskTreeStopPromise = drainOwnedWindowsAgentTree(child);
149
+ child.livedeskTreeStopPromise = drainOwnedWindowsAgentTreeUntilStopped(child);
85
150
  }
86
151
  return true;
87
152
  }
@@ -213,9 +278,12 @@ Default flow:
213
278
  Options:
214
279
  --slot <number> Screen wall slot number for this computer.
215
280
  --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.
281
+ --engine auto|fast|csharp|node Select agent engine. Default: auto.
282
+ --fast Force C# RemoteFast frame.binary engine.
283
+ --node Force the legacy Node agent.
284
+ --transport auto|tcp|ws|udp-p2p
285
+ Direct-first connection policy. Default: auto.
286
+ --relay <host:port> Encrypted fallback relay. Default: www.gnsoftech.com:5199.
219
287
  --control Enable RemoteFast keyboard/mouse control.
220
288
  --no-control Disable RemoteFast keyboard/mouse control.
221
289
  --thumbnail Enable thumbnail capture when supported.
@@ -247,7 +315,7 @@ function isTruthy(value) {
247
315
  return /^(1|true|yes|on)$/i.test(String(value || '').trim());
248
316
  }
249
317
 
250
- function normalizeEngine(value) {
318
+ function normalizeEngine(value) {
251
319
  const engine = String(value || 'auto').trim().toLowerCase();
252
320
  if (engine === 'c#' || engine === 'csharp' || engine === 'fast') {
253
321
  return 'fast';
@@ -255,11 +323,54 @@ function normalizeEngine(value) {
255
323
  if (engine === 'js' || engine === 'node' || engine === 'legacy') {
256
324
  return 'node';
257
325
  }
258
- return 'auto';
259
- }
260
-
261
- function parseLauncherArgs(argv) {
262
- const forwarded = [];
326
+ return 'auto';
327
+ }
328
+
329
+ function normalizeClientTransport(value) {
330
+ const transport = String(value || 'auto').trim().toLowerCase();
331
+ if (transport === 'legacy') return 'tcp';
332
+ if (transport === 'wss') return 'ws';
333
+ if (transport === 'udp-relay') return 'udp-p2p';
334
+ return ['auto', 'tcp', 'ws', 'udp-p2p'].includes(transport)
335
+ ? transport
336
+ : 'tcp';
337
+ }
338
+
339
+ export function parseClientTransportOption(value) {
340
+ const transport = String(value || '').trim().toLowerCase();
341
+ if (!['auto', 'tcp', 'legacy', 'ws', 'wss', 'udp-p2p', 'udp-relay'].includes(transport)) {
342
+ throw new Error(`Unsupported LiveDesk Client transport: ${value || '(missing)'}`);
343
+ }
344
+ return normalizeClientTransport(transport);
345
+ }
346
+
347
+ function transportAllowsRelay(transport) {
348
+ return transport === 'auto' || transport === 'udp-p2p';
349
+ }
350
+
351
+ export function resolveClientRelayEndpoint(env = process.env) {
352
+ const explicit = String(
353
+ env.LIVEDESK_CLIENT_RELAY
354
+ || env.LIVEDESK_RELAY_ENDPOINT
355
+ || ''
356
+ ).trim().replace(/^tcp:\/\//i, '');
357
+ if (parseManagerEndpoint(explicit)) {
358
+ return explicit;
359
+ }
360
+ const host = String(
361
+ env.LIVEDESK_RELAY_HOST
362
+ || env.LIVEDESK_UDP_RENDEZVOUS_HOST
363
+ || DEFAULT_RELAY_HOST
364
+ ).trim();
365
+ const port = normalizePort(
366
+ env.LIVEDESK_RELAY_PORT
367
+ || env.LIVEDESK_UDP_RENDEZVOUS_PORT
368
+ ) || DEFAULT_RELAY_PORT;
369
+ return host ? `${host}:${port}` : '';
370
+ }
371
+
372
+ export function parseLauncherArgs(argv) {
373
+ const forwarded = [];
263
374
  let engine = normalizeEngine(process.env.LIVEDESK_CLIENT_ENGINE || process.env.MINDEXEC_REMOTE_ENGINE);
264
375
  let help = false;
265
376
  let loginRequested = false;
@@ -267,7 +378,18 @@ function parseLauncherArgs(argv) {
267
378
  let logout = false;
268
379
  let version = false;
269
380
  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 || '';
381
+ let pair = process.env.LIVEDESK_CLIENT_PAIR_TOKEN || process.env.MINDEXEC_REMOTE_PAIR_TOKEN || '';
382
+ let transport = normalizeClientTransport(
383
+ process.env.LIVEDESK_CLIENT_TRANSPORT || process.env.MINDEXEC_REMOTE_TRANSPORT);
384
+ let relay = resolveClientRelayEndpoint();
385
+ const requireFast = isTruthy(process.env.LIVEDESK_CLIENT_REQUIRE_FAST);
386
+ let relayExplicit = Boolean(String(
387
+ process.env.LIVEDESK_CLIENT_RELAY
388
+ || process.env.LIVEDESK_RELAY_ENDPOINT
389
+ || process.env.LIVEDESK_RELAY_HOST
390
+ || process.env.LIVEDESK_UDP_RENDEZVOUS_HOST
391
+ || ''
392
+ ).trim());
271
393
  let deviceId = String(process.env.LIVEDESK_DEVICE_ID || '').trim();
272
394
  let slot = process.env.LIVEDESK_CLIENT_SLOT || process.env.MINDEXEC_REMOTE_SLOT || '';
273
395
  let authPort = normalizePort(process.env.LIVEDESK_CLIENT_RUNTIME_PORT || process.env.LIVEDESK_CLIENT_AUTH_PORT) || DEFAULT_AUTH_CALLBACK_PORT;
@@ -347,13 +469,28 @@ function parseLauncherArgs(argv) {
347
469
  index += 1;
348
470
  continue;
349
471
  }
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') {
472
+ if (arg === '--pair') {
473
+ pair = argv[index + 1] || pair;
474
+ forwarded.push(arg, argv[index + 1]);
475
+ index += 1;
476
+ continue;
477
+ }
478
+ if (arg === '--transport') {
479
+ transport = parseClientTransportOption(argv[index + 1]);
480
+ index += 1;
481
+ continue;
482
+ }
483
+ if (arg === '--relay') {
484
+ const candidate = String(argv[index + 1] || '').trim().replace(/^tcp:\/\//i, '');
485
+ if (!parseManagerEndpoint(candidate)) {
486
+ throw new Error(`Invalid LiveDesk relay endpoint: ${argv[index + 1] || '(missing)'}`);
487
+ }
488
+ relay = candidate;
489
+ relayExplicit = true;
490
+ index += 1;
491
+ continue;
492
+ }
493
+ if (arg === '--device-id') {
357
494
  deviceId = argv[index + 1] || deviceId;
358
495
  forwarded.push(arg, argv[index + 1]);
359
496
  index += 1;
@@ -393,9 +530,13 @@ function parseLauncherArgs(argv) {
393
530
  loginDisabled,
394
531
  logout,
395
532
  version,
396
- manager,
397
- pair,
398
- deviceId,
533
+ manager,
534
+ pair,
535
+ transport,
536
+ relay,
537
+ relayExplicit,
538
+ requireFast,
539
+ deviceId,
399
540
  slot,
400
541
  authPort,
401
542
  nodeOnlyFeature,
@@ -573,13 +714,19 @@ function buildStartupClientArgs(parsed) {
573
714
  if (slot) {
574
715
  args.push(slot);
575
716
  }
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 : [];
717
+ if (parsed.engine === 'fast') {
718
+ args.push('--fast');
719
+ } else if (parsed.engine === 'node') {
720
+ args.push('--node');
721
+ }
722
+ if (parsed.transport && parsed.transport !== 'auto') {
723
+ args.push('--transport', parsed.transport);
724
+ }
725
+ if (parsed.relayExplicit && parsed.relay) {
726
+ args.push('--relay', parsed.relay);
727
+ }
728
+
729
+ const forwarded = Array.isArray(parsed.forwarded) ? parsed.forwarded : [];
583
730
  for (let index = 0; index < forwarded.length; index += 1) {
584
731
  const arg = forwarded[index];
585
732
  if (!arg || arg === 'connect') {
@@ -2471,7 +2618,7 @@ function readRequestBody(req, maxBytes = 4096) {
2471
2618
  });
2472
2619
  }
2473
2620
 
2474
- async function resolveManagerFromPin(supabase, pin) {
2621
+ async function resolveManagerFromPin(supabase, pin, options = {}) {
2475
2622
  const normalizedPin = normalizePairingPin(pin);
2476
2623
  if (!normalizedPin) {
2477
2624
  throw new Error('Enter a 6-digit LiveDesk PIN.');
@@ -2500,16 +2647,20 @@ async function resolveManagerFromPin(supabase, pin) {
2500
2647
  if (endpointCandidates.length === 0) {
2501
2648
  throw new Error('The PIN matched a Hub record without a reachable address.');
2502
2649
  }
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
-
2650
+ const target = await chooseManagerConnectionTarget(endpointCandidates, {
2651
+ allowRelayFallback: options.allowRelayFallback === true
2652
+ });
2653
+ if (!target) {
2654
+ throw new Error(`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`);
2655
+ }
2656
+
2508
2657
  const resolved = {
2509
- manager,
2658
+ manager: target.manager,
2510
2659
  pair: result.pair_token,
2511
2660
  hubDeviceId: String(result.node_id || '').trim(),
2512
2661
  endpointCandidates,
2662
+ directReachable: target.directReachable,
2663
+ connectionTransport: target.connectionTransport,
2513
2664
  discoverySource: 'supabase'
2514
2665
  };
2515
2666
  writeCachedHubTarget(cacheOwnerKey, resolved);
@@ -2528,7 +2679,9 @@ async function waitForManagerFromSavedPin(supabase, pin, options = {}) {
2528
2679
  }
2529
2680
  attempts += 1;
2530
2681
  try {
2531
- return await resolveManagerFromPin(supabase, pin);
2682
+ return await resolveManagerFromPin(supabase, pin, {
2683
+ allowRelayFallback: options.allowRelayFallback === true
2684
+ });
2532
2685
  } catch (err) {
2533
2686
  if (shouldStop()) {
2534
2687
  return null;
@@ -2912,7 +3065,9 @@ async function startConnectionChoiceServer(supabase, options = {}) {
2912
3065
  pendingStartup = normalizeStartupChoice(startupValue, pendingStartup);
2913
3066
  pin = normalizePairingPin(pin);
2914
3067
  try {
2915
- const resolved = await resolveManagerFromPin(supabase, pin);
3068
+ const resolved = await resolveManagerFromPin(supabase, pin, {
3069
+ allowRelayFallback: options.allowRelayFallback === true
3070
+ });
2916
3071
  writeSavedPin(pin);
2917
3072
  applyStartupPreference(pendingStartup, startupArgs);
2918
3073
  const choice = { type: 'pin', ...resolved };
@@ -3073,7 +3228,9 @@ async function chooseClientConnection(supabase, options = {}) {
3073
3228
  },
3074
3229
  resolvePin: pin => {
3075
3230
  if (!supabase) throw new Error('supabase-session-required');
3076
- return resolveManagerFromPin(supabase, pin);
3231
+ return resolveManagerFromPin(supabase, pin, {
3232
+ allowRelayFallback: options.allowRelayFallback === true
3233
+ });
3077
3234
  },
3078
3235
  changeRole: async (role, snapshot) => {
3079
3236
  if (!supabase) {
@@ -3139,8 +3296,8 @@ async function chooseClientConnection(supabase, options = {}) {
3139
3296
  }, 150);
3140
3297
  return { ...result, slotNumber, saved: true, restarting: true };
3141
3298
  },
3142
- onRestart: () => process.kill(process.pid, 'SIGTERM'),
3143
- onShutdown: () => process.kill(process.pid, 'SIGTERM'),
3299
+ onRestart: () => requestClientRuntimeShutdown('SIGTERM'),
3300
+ onShutdown: () => requestClientRuntimeShutdown('SIGTERM'),
3144
3301
  onDisconnect: () => requestActiveAgentStop(),
3145
3302
  // Ending the current Agent causes the unified lifecycle loop to
3146
3303
  // discard its old manager/pair token and start Supabase discovery
@@ -3169,9 +3326,10 @@ async function chooseClientConnection(supabase, options = {}) {
3169
3326
  deviceId: options.deviceId,
3170
3327
  slot: options.slot,
3171
3328
  startupArgs: options.startupArgs,
3172
- savedSession: options.savedSession,
3173
- savedPin: options.savedPin
3174
- });
3329
+ savedSession: options.savedSession,
3330
+ savedPin: options.savedPin,
3331
+ allowRelayFallback: options.allowRelayFallback === true
3332
+ });
3175
3333
  if (options.openBrowser !== false) {
3176
3334
  console.log('Opening LiveDesk connection page...');
3177
3335
  openBrowser(connectionPage.url);
@@ -3186,11 +3344,11 @@ function parseManagerEndpoint(value) {
3186
3344
  if (separator <= 0) {
3187
3345
  return null;
3188
3346
  }
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
- }
3347
+ const host = text.slice(0, separator).replace(/^\[/, '').replace(/\]$/, '').trim();
3348
+ const port = Number(text.slice(separator + 1));
3349
+ if (!host || !Number.isInteger(port) || port < 1 || port > 65535) {
3350
+ return null;
3351
+ }
3194
3352
  return { host, port };
3195
3353
  }
3196
3354
 
@@ -3316,6 +3474,37 @@ export async function chooseReachableEndpoint(candidates, options = {}) {
3316
3474
  });
3317
3475
  }
3318
3476
 
3477
+ export async function chooseManagerConnectionTarget(candidates, options = {}) {
3478
+ const endpoints = [...new Set((Array.isArray(candidates) ? candidates : [])
3479
+ .map(value => String(value || '').trim())
3480
+ .filter(Boolean))];
3481
+ if (endpoints.length === 0) return null;
3482
+
3483
+ const manager = await chooseReachableEndpoint(endpoints, {
3484
+ requireReachable: true,
3485
+ probeEndpoint: options.probeEndpoint
3486
+ });
3487
+ if (manager) {
3488
+ return {
3489
+ manager,
3490
+ directReachable: true,
3491
+ connectionTransport: 'direct-tcp'
3492
+ };
3493
+ }
3494
+ if (options.allowRelayFallback !== true) {
3495
+ return null;
3496
+ }
3497
+ return {
3498
+ // The account/PIN response is the authority for this retry candidate.
3499
+ // RemoteFast gives it one bounded direct attempt, then uses the
3500
+ // pair-token-authenticated encrypted relay rather than keeping the
3501
+ // launcher in an indefinite TCP reachability loop.
3502
+ manager: endpoints[0],
3503
+ directReachable: false,
3504
+ connectionTransport: 'relay-fallback'
3505
+ };
3506
+ }
3507
+
3319
3508
  export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
3320
3509
  const cached = options.cachedTarget || readCachedHubTarget(ownerKey);
3321
3510
  if (!cached) return null;
@@ -3411,15 +3600,19 @@ async function resolveManagerFromSupabase(supabase, options = {}) {
3411
3600
  if (endpointCandidates.length === 0) {
3412
3601
  throw new Error('The LiveDesk Hub record does not contain a reachable local address.');
3413
3602
  }
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
3603
+ const target = await chooseManagerConnectionTarget(endpointCandidates, {
3604
+ allowRelayFallback: options.allowRelayFallback === true
3605
+ });
3606
+ if (!target) {
3607
+ throw new Error(`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`);
3608
+ }
3609
+ return {
3610
+ manager: target.manager,
3611
+ pair: data.pair_token,
3612
+ hubDeviceId: String(data.node_id || '').trim(),
3613
+ endpointCandidates,
3614
+ directReachable: target.directReachable,
3615
+ connectionTransport: target.connectionTransport
3423
3616
  };
3424
3617
  }
3425
3618
 
@@ -3462,7 +3655,9 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
3462
3655
  }
3463
3656
  attempts += 1;
3464
3657
  try {
3465
- return await resolveManagerFromSupabase(supabase, { requireReachable: true });
3658
+ return await resolveManagerFromSupabase(supabase, {
3659
+ allowRelayFallback: options.allowRelayFallback === true
3660
+ });
3466
3661
  } catch (err) {
3467
3662
  const message = formatDiscoveryError(err);
3468
3663
  if (message !== lastMessage || attempts === 1 || attempts % 6 === 0) {
@@ -3515,6 +3710,9 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3515
3710
  let pair = parsed.pair;
3516
3711
  let connectionPage = null;
3517
3712
  let discoverySource = '';
3713
+ let connectionTransport = '';
3714
+ let directReachable = false;
3715
+ const allowRelayFallback = transportAllowsRelay(parsed.transport);
3518
3716
 
3519
3717
  if (shouldLogin) {
3520
3718
  const supabase = await createSupabaseClient();
@@ -3531,7 +3729,9 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3531
3729
  if ((parsed.startupRun || existingConnectionPage) && savedSession?.access_token) {
3532
3730
  choice = { type: 'google', session: savedSession };
3533
3731
  } else if ((parsed.startupRun || existingConnectionPage) && savedPin) {
3534
- const resolved = await waitForManagerFromSavedPin(supabase, savedPin);
3732
+ const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
3733
+ allowRelayFallback
3734
+ });
3535
3735
  choice = { type: 'pin', ...resolved };
3536
3736
  } else if (existingConnectionPage && previousChoice) {
3537
3737
  choice = previousChoice;
@@ -3543,10 +3743,11 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3543
3743
  deviceId: parsed.deviceId,
3544
3744
  engine: parsed.engine,
3545
3745
  slot: parsed.slot,
3546
- startupArgs,
3547
- savedSession,
3548
- savedPin,
3549
- openBrowser: parsed.openBrowserOnStart
3746
+ startupArgs,
3747
+ savedSession,
3748
+ savedPin,
3749
+ allowRelayFallback,
3750
+ openBrowser: parsed.openBrowserOnStart
3550
3751
  });
3551
3752
  }
3552
3753
  connectionPage = existingConnectionPage || choice.connectionPage || null;
@@ -3554,19 +3755,27 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3554
3755
  manager = choice.manager;
3555
3756
  pair = choice.pair;
3556
3757
  discoverySource = choice.discoverySource || 'supabase';
3758
+ connectionTransport = choice.connectionTransport || 'direct-tcp';
3759
+ directReachable = choice.directReachable !== false;
3557
3760
  connectionPage?.update({
3558
3761
  endpointCandidates: choice.endpointCandidates || [],
3559
3762
  manager,
3560
3763
  assignedHubId: choice.hubDeviceId || '',
3561
- message: `Connected to LiveDesk Hub at ${manager}.`
3562
- });
3563
- console.log('Connected by LiveDesk PIN.');
3764
+ message: connectionTransport === 'relay-fallback'
3765
+ ? `LiveDesk Hub found at ${manager}; starting encrypted relay fallback.`
3766
+ : `Connected to LiveDesk Hub at ${manager}.`
3767
+ });
3768
+ console.log(connectionTransport === 'relay-fallback'
3769
+ ? 'LiveDesk PIN accepted. Direct TCP is unavailable; starting encrypted relay fallback.'
3770
+ : 'Connected by LiveDesk PIN.');
3564
3771
  } else {
3565
3772
  const cacheOwnerKey = hubCacheOwnerForSession(choice.session);
3566
3773
  let resolved = await resolveManagerFromCachedTarget(cacheOwnerKey);
3567
3774
  let session = choice.session;
3568
3775
  discoverySource = resolved ? 'cache' : 'supabase';
3569
3776
  if (resolved) {
3777
+ connectionTransport = resolved.connectionTransport || 'direct-tcp';
3778
+ directReachable = resolved.directReachable !== false;
3570
3779
  writeSavedSessionToFile(session);
3571
3780
  console.log(`Found cached LiveDesk Hub at ${resolved.manager}; skipped registry discovery.`);
3572
3781
  } else {
@@ -3587,6 +3796,7 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3587
3796
  console.log(`Signed in to LiveDesk${email}.`);
3588
3797
  if (!resolved) {
3589
3798
  resolved = await waitForManagerFromSupabase(supabase, {
3799
+ allowRelayFallback,
3590
3800
  shouldStop: () => Boolean(roleRestartRequest?.role)
3591
3801
  });
3592
3802
  }
@@ -3615,15 +3825,21 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3615
3825
  }
3616
3826
  }
3617
3827
  manager = resolved.manager;
3618
- pair = resolved.pair;
3828
+ pair = resolved.pair;
3829
+ connectionTransport = resolved.connectionTransport || 'direct-tcp';
3830
+ directReachable = resolved.directReachable !== false;
3619
3831
  connectionPage?.update({
3620
3832
  endpointCandidates: resolved.endpointCandidates || [],
3621
3833
  manager,
3622
3834
  assignedHubId: resolved.hubDeviceId || '',
3623
- message: `Connected to LiveDesk Hub at ${manager}.`
3624
- });
3625
- }
3626
- console.log(`Found LiveDesk Hub at ${manager}.`);
3835
+ message: connectionTransport === 'relay-fallback'
3836
+ ? `LiveDesk Hub found at ${manager}; starting encrypted relay fallback.`
3837
+ : `Connected to LiveDesk Hub at ${manager}.`
3838
+ });
3839
+ }
3840
+ console.log(connectionTransport === 'relay-fallback'
3841
+ ? `Found LiveDesk Hub record at ${manager}; direct TCP did not answer, so RemoteFast will use the encrypted rendezvous relay.`
3842
+ : `Found LiveDesk Hub at ${manager}.`);
3627
3843
  }
3628
3844
  // Exact-package updates preserve the already paired Hub endpoint and pass
3629
3845
  // --no-login so the replacement cannot block on browser auth. The unified
@@ -3669,12 +3885,16 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
3669
3885
  forwarded = appendForwardedFlag(forwarded, '--exit-on-invalid-pair');
3670
3886
  }
3671
3887
  return {
3672
- ...parsed,
3673
- manager,
3674
- pair,
3675
- slot,
3888
+ ...parsed,
3889
+ manager,
3890
+ pair,
3891
+ transport: parsed.transport,
3892
+ relay: parsed.relay,
3893
+ slot,
3676
3894
  connectionPage,
3677
3895
  forwarded,
3896
+ connectionTransport: connectionTransport || 'direct-tcp',
3897
+ directReachable: connectionTransport === 'relay-fallback' ? false : (directReachable || !shouldLogin),
3678
3898
  discoverySource,
3679
3899
  rediscoverOnDisconnect: shouldLogin,
3680
3900
  rediscoverOnInvalidPair: shouldLogin
@@ -3894,7 +4114,9 @@ function buildClientAgentEnvironment(prepared) {
3894
4114
  env.LIVEDESK_CLIENT_SLOT = String(prepared?.slot || env.LIVEDESK_CLIENT_SLOT || '');
3895
4115
  env.LIVEDESK_DEVICE_ID = String(prepared?.deviceId || env.LIVEDESK_DEVICE_ID || '');
3896
4116
  env.LIVEDESK_CLIENT_ENGINE = String(prepared?.engine || env.LIVEDESK_CLIENT_ENGINE || 'auto');
3897
- env.LIVEDESK_CLIENT_TRANSPORT = String(env.LIVEDESK_CLIENT_TRANSPORT || 'auto');
4117
+ env.LIVEDESK_CLIENT_TRANSPORT = String(prepared?.transport || env.LIVEDESK_CLIENT_TRANSPORT || 'auto');
4118
+ env.LIVEDESK_CLIENT_RELAY = String(prepared?.relay || env.LIVEDESK_CLIENT_RELAY || '');
4119
+ env.LIVEDESK_CLIENT_REQUIRE_FAST = requiresFastTransport(prepared) ? '1' : '0';
3898
4120
  env.LIVEDESK_UDP_ENABLED = String(env.LIVEDESK_UDP_ENABLED || '1');
3899
4121
  env.LIVEDESK_CLIENT_UPDATE_STATE_PATH = CLIENT_UPDATE_STATE_PATH;
3900
4122
  env.LIVEDESK_CLIENT_UPDATE_ARGS_BASE64 = Buffer
@@ -4007,23 +4229,35 @@ function prepareFastExecutable(executable) {
4007
4229
  }
4008
4230
  }
4009
4231
 
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;
4232
+ export function buildFastArgs(args, fakeThumbnail, transport = 'auto', relay = '') {
4233
+ const forwarded = [];
4234
+ for (let index = 0; index < args.length; index += 1) {
4235
+ const arg = args[index];
4236
+ if (arg === '--transport' || arg === '--relay') {
4237
+ index += 1;
4238
+ continue;
4239
+ }
4240
+ if (arg === '--fake-thumbnail') {
4241
+ forwarded.push('--fake-frame');
4242
+ continue;
4016
4243
  }
4017
4244
  if (arg === '--tasks' || arg === '--no-tasks' || arg === '--no-ai') {
4018
4245
  continue;
4019
4246
  }
4020
4247
  forwarded.push(arg);
4021
4248
  }
4022
- if (fakeThumbnail && !forwarded.includes('--fake-frame')) {
4023
- forwarded.push('--fake-frame');
4024
- }
4025
- return forwarded;
4026
- }
4249
+ if (fakeThumbnail && !forwarded.includes('--fake-frame')) {
4250
+ forwarded.push('--fake-frame');
4251
+ }
4252
+ let launchArgs = upsertForwardedOption(
4253
+ forwarded,
4254
+ '--transport',
4255
+ normalizeClientTransport(transport));
4256
+ if (transportAllowsRelay(normalizeClientTransport(transport))) {
4257
+ launchArgs = upsertForwardedOption(launchArgs, '--relay', relay);
4258
+ }
4259
+ return launchArgs;
4260
+ }
4027
4261
 
4028
4262
  function redactAgentArgs(args = []) {
4029
4263
  const redacted = [];
@@ -4137,7 +4371,10 @@ function spawnAgent(command, args, env = process.env, onStart = null) {
4137
4371
  });
4138
4372
  child.livedeskOwnsProcessGroup = ownsProcessGroup;
4139
4373
  child.livedeskWindowsTreeTracker = createWindowsProcessTreeTracker(child, {
4140
- spawnedAtMs: agentSpawnedAtMs
4374
+ spawnedAtMs: agentSpawnedAtMs,
4375
+ publishTrackedRecords: WINDOWS_PROCESS_MANIFEST_OWNER
4376
+ ? publishWindowsOwnedAgentRecords
4377
+ : null
4141
4378
  });
4142
4379
  activeAgentProcess = child;
4143
4380
  if (typeof onStart === 'function') {
@@ -4169,7 +4406,7 @@ function spawnAgent(command, args, env = process.env, onStart = null) {
4169
4406
  try {
4170
4407
  if (process.platform === 'win32') {
4171
4408
  if (!child.livedeskTreeStopPromise) {
4172
- child.livedeskTreeStopPromise = drainOwnedWindowsAgentTree(child);
4409
+ child.livedeskTreeStopPromise = drainOwnedWindowsAgentTreeUntilStopped(child);
4173
4410
  }
4174
4411
  const windowsTreeStop = await child.livedeskTreeStopPromise;
4175
4412
  if (windowsTreeStop?.ok === false) {
@@ -4219,12 +4456,20 @@ function shouldUseFast(parsed) {
4219
4456
  return !parsed.nodeOnlyFeature;
4220
4457
  }
4221
4458
 
4222
- function shouldTryFast(parsed, runtime) {
4459
+ function shouldTryFast(parsed, runtime) {
4223
4460
  if (parsed.engine === 'fast') {
4224
4461
  return true;
4225
4462
  }
4226
4463
  return shouldUseFast(parsed) && !!runtime;
4227
- }
4464
+ }
4465
+
4466
+ export function requiresFastTransport(prepared = {}) {
4467
+ return prepared.requireFast === true
4468
+ || prepared.connectionTransport === 'relay-fallback'
4469
+ || prepared.transport === 'udp-p2p'
4470
+ || prepared.transport === 'ws'
4471
+ || prepared.relayExplicit === true;
4472
+ }
4228
4473
 
4229
4474
  export async function runClientRuntime(argv = process.argv.slice(2)) {
4230
4475
  startNetworkChangeMonitor();
@@ -4278,9 +4523,16 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
4278
4523
  await spawnUnifiedRoleRestart(roleRestartBeforeAgent.role);
4279
4524
  connectionPage?.close?.();
4280
4525
  process.exit(0);
4281
- }
4526
+ }
4282
4527
  const fastRuntime = getFastRuntime();
4283
- const useFast = shouldTryFast(prepared, fastRuntime);
4528
+ const fastRequired = requiresFastTransport(prepared);
4529
+ if (fastRequired && prepared.engine === 'node') {
4530
+ throw new Error(
4531
+ 'The legacy Node engine cannot use WebSocket, UDP P2P, or encrypted relay transport. '
4532
+ + 'Use --engine fast, or use --transport tcp while the Hub is directly reachable.'
4533
+ );
4534
+ }
4535
+ const useFast = fastRequired || shouldTryFast(prepared, fastRuntime);
4284
4536
  const agentLaunchStartedAt = Date.now();
4285
4537
  const updateAgentDashboard = (patch = {}) => {
4286
4538
  prepared.connectionPage?.update({
@@ -4294,11 +4546,15 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
4294
4546
  ? `LiveDesk client agent is running with ${patch.engine || 'agent'} mode.`
4295
4547
  : 'LiveDesk client agent is launching.'
4296
4548
  });
4297
- };
4298
- let result;
4299
- if (useFast) {
4300
- const fastArgs = buildFastArgs(prepared.forwarded, prepared.fakeThumbnail);
4301
- const fastLaunch = resolveFastLaunch(fastRuntime);
4549
+ };
4550
+ let result;
4551
+ if (useFast) {
4552
+ const fastArgs = buildFastArgs(
4553
+ prepared.forwarded,
4554
+ prepared.fakeThumbnail,
4555
+ prepared.transport,
4556
+ prepared.relay);
4557
+ const fastLaunch = resolveFastLaunch(fastRuntime);
4302
4558
  if (fastLaunch.ok) {
4303
4559
  const launchArgs = [...fastLaunch.argsPrefix, ...fastArgs];
4304
4560
  updateAgentDashboard({
@@ -4319,10 +4575,15 @@ export async function runClientRuntime(argv = process.argv.slice(2)) {
4319
4575
  state: 'running'
4320
4576
  });
4321
4577
  });
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);
4578
+ } else if (prepared.engine === 'fast' || fastRequired) {
4579
+ console.error(
4580
+ `C# RemoteFast is unavailable: ${fastLaunch.reason}. `
4581
+ + (fastRequired
4582
+ ? 'The requested P2P, WebSocket, or encrypted relay transport requires the packaged RemoteFast runtime; the legacy Node agent was not started.'
4583
+ : 'Use --engine node to run the legacy Node agent.')
4584
+ );
4585
+ prepared.connectionPage?.close?.();
4586
+ process.exit(2);
4326
4587
  } else {
4327
4588
  console.warn(`C# RemoteFast is unavailable (${fastLaunch.reason}). Falling back to the Node remote agent.`);
4328
4589
  const launchArgs = [nodeAgentPath, ...prepared.forwarded];