livedesk 0.1.449 → 0.1.451
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.
- package/bin/livedesk.js +4 -0
- package/client/bin/livedesk-client.js +314 -105
- package/client/package.json +5 -5
- package/client/src/runtime/client-runtime-server.js +19 -4
- package/hub/package.json +1 -1
- package/hub/src/agents/agent-manager.js +5 -2
- package/hub/src/agents/codex-agent-runtime.js +10 -13
- package/hub/src/remote-hub.js +19 -6
- package/hub/src/server.js +5 -3
- package/hub/src/transport/udp-hub-transport.js +22 -22
- package/hub/src/transport/udp-protocol.js +51 -26
- package/package.json +6 -6
- package/web/dist/assets/{index-BPpXtm8K.css → index-9QTFmh8R.css} +1 -1
- package/web/dist/assets/{index-CDA-kp_V.js → index-BFwWY4pa.js} +12 -12
- package/web/dist/index.html +2 -2
package/bin/livedesk.js
CHANGED
|
@@ -2794,6 +2794,10 @@ async function runClient(args, resolvedRole = null, runtimeLock = null) {
|
|
|
2794
2794
|
: String(process.env.LIVEDESK_SKIP_BROWSER_OPEN || ''),
|
|
2795
2795
|
LIVEDESK_DEVICE_ID: String(resolvedRole?.deviceId || process.env.LIVEDESK_DEVICE_ID || ''),
|
|
2796
2796
|
LIVEDESK_ROLE_SOURCE: String(resolvedRole?.source || process.env.LIVEDESK_ROLE_SOURCE || ''),
|
|
2797
|
+
LIVEDESK_ASSIGNED_HUB_ID: String(resolvedRole?.assignedHubId || ''),
|
|
2798
|
+
LIVEDESK_ROLE_VERSION: Number.isInteger(resolvedRole?.roleVersion) && resolvedRole.roleVersion >= 0
|
|
2799
|
+
? String(resolvedRole.roleVersion)
|
|
2800
|
+
: '0',
|
|
2797
2801
|
LIVEDESK_RUNTIME_OWNER_PID: String(runtimeOwner?.pid || ''),
|
|
2798
2802
|
LIVEDESK_RUNTIME_OWNER_TOKEN: String(runtimeOwner?.ownerToken || ''),
|
|
2799
2803
|
LIVEDESK_RUNTIME_OWNER_INSTANCE_MARKER: String(runtimeOwner?.ownerInstanceMarker || ''),
|
|
@@ -39,6 +39,9 @@ const DEFAULT_RELAY_PORT = 5199;
|
|
|
39
39
|
const DEFAULT_AUTH_CALLBACK_HOST = '127.0.0.1';
|
|
40
40
|
const DEFAULT_AUTH_CALLBACK_PORT = 5179;
|
|
41
41
|
const ENDPOINT_PROBE_TIMEOUT_MS = 900;
|
|
42
|
+
const FRESH_REGISTRY_TIMEOUT_MS = 4000;
|
|
43
|
+
const FRESH_REGISTRY_TIMEOUT_MIN_MS = 25;
|
|
44
|
+
const FRESH_REGISTRY_TIMEOUT_MAX_MS = 10000;
|
|
42
45
|
// Hub-online delivery is an optimization, not a correctness dependency. A
|
|
43
46
|
// browser, proxy, or suspended network can miss the wake event, so registry
|
|
44
47
|
// polling must stay interactive instead of backing off to multi-minute waits.
|
|
@@ -49,6 +52,7 @@ const EXIT_INVALID_PAIR_TOKEN = 23;
|
|
|
49
52
|
const EXIT_CLIENT_UPDATE = 42;
|
|
50
53
|
const SESSION_REFRESH_SKEW_SECONDS = 60;
|
|
51
54
|
const HUB_TARGET_CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
55
|
+
const HUB_TARGET_CACHE_FUTURE_SKEW_MS = 5 * 60 * 1000;
|
|
52
56
|
const WINDOWS_STARTUP_SCRIPT_NAME = 'LiveDesk Desktop.vbs';
|
|
53
57
|
const WINDOWS_STARTUP_LEGACY_SCRIPT_NAME = 'LiveDesk Client.vbs';
|
|
54
58
|
const WINDOWS_STARTUP_LEGACY_CMD_NAME = 'LiveDesk Client.cmd';
|
|
@@ -967,7 +971,9 @@ export function normalizeCachedHubTarget(value, expectedOwnerKey, now = Date.now
|
|
|
967
971
|
return null;
|
|
968
972
|
}
|
|
969
973
|
const updatedAt = Date.parse(String(cached.updatedAt || ''));
|
|
970
|
-
if (!Number.isFinite(updatedAt)
|
|
974
|
+
if (!Number.isFinite(updatedAt)
|
|
975
|
+
|| updatedAt > now + HUB_TARGET_CACHE_FUTURE_SKEW_MS
|
|
976
|
+
|| now - updatedAt > HUB_TARGET_CACHE_MAX_AGE_MS) {
|
|
971
977
|
return null;
|
|
972
978
|
}
|
|
973
979
|
const pair = String(cached.pair || '').trim();
|
|
@@ -1131,13 +1137,63 @@ function isTransientNetworkError(error) {
|
|
|
1131
1137
|
return /fetch failed|network|dns|socket|connection|timeout|getaddrinfo/i.test(getNestedErrorMessage(error));
|
|
1132
1138
|
}
|
|
1133
1139
|
|
|
1134
|
-
function formatDiscoveryError(error) {
|
|
1135
|
-
if (isTransientNetworkError(error)) {
|
|
1136
|
-
const code = getNestedErrorCode(error);
|
|
1137
|
-
return `Network is not ready yet${code ? ` (${code})` : ''}. Waiting for DNS/Wi-Fi after sleep.`;
|
|
1138
|
-
}
|
|
1139
|
-
return error instanceof Error ? error.message : String(error);
|
|
1140
|
-
}
|
|
1140
|
+
function formatDiscoveryError(error) {
|
|
1141
|
+
if (isTransientNetworkError(error)) {
|
|
1142
|
+
const code = getNestedErrorCode(error);
|
|
1143
|
+
return `Network is not ready yet${code ? ` (${code})` : ''}. Waiting for DNS/Wi-Fi after sleep.`;
|
|
1144
|
+
}
|
|
1145
|
+
return error instanceof Error ? error.message : String(error);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function createHubDiscoveryError(code, message, cause = null) {
|
|
1149
|
+
const error = new Error(message);
|
|
1150
|
+
error.code = String(code || '').trim();
|
|
1151
|
+
if (cause) {
|
|
1152
|
+
error.cause = cause;
|
|
1153
|
+
}
|
|
1154
|
+
return error;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
export function canUseCachedRelayAfterRegistryError(error) {
|
|
1158
|
+
const code = getNestedErrorCode(error).toLowerCase();
|
|
1159
|
+
if ([
|
|
1160
|
+
'hub-registry-missing',
|
|
1161
|
+
'hub-registry-expired',
|
|
1162
|
+
'hub-registry-timeout',
|
|
1163
|
+
'pin-not-found',
|
|
1164
|
+
'pin-expired',
|
|
1165
|
+
'resolver-unavailable'
|
|
1166
|
+
].includes(code)) {
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
// Semantic registry failures must never be reclassified as network errors
|
|
1170
|
+
// merely because their message mentions a "connection token".
|
|
1171
|
+
if (code.startsWith('hub-registry-')
|
|
1172
|
+
|| code.startsWith('pin-registry-')
|
|
1173
|
+
|| ['invalid-pin', 'rate-limited', 'request-too-large'].includes(code)) {
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
return isTransientNetworkError(error);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
async function normalizePinResolverInvocationError(error) {
|
|
1180
|
+
if (isTransientNetworkError(error)) {
|
|
1181
|
+
return error;
|
|
1182
|
+
}
|
|
1183
|
+
const response = error?.context;
|
|
1184
|
+
const status = Number(response?.status || 0);
|
|
1185
|
+
let reason = '';
|
|
1186
|
+
try {
|
|
1187
|
+
const payload = await response?.clone?.().json?.();
|
|
1188
|
+
reason = String(payload?.reason || '').trim().toLowerCase();
|
|
1189
|
+
} catch {
|
|
1190
|
+
// HTTP status still gives a conservative semantic classification below.
|
|
1191
|
+
}
|
|
1192
|
+
if (!reason && status === 404) reason = 'pin-not-found';
|
|
1193
|
+
if (!reason && status === 503) reason = 'resolver-unavailable';
|
|
1194
|
+
if (!reason) return error;
|
|
1195
|
+
return createHubDiscoveryError(reason, reason, error);
|
|
1196
|
+
}
|
|
1141
1197
|
|
|
1142
1198
|
async function recoverRotatedSession(supabase, previousSession) {
|
|
1143
1199
|
for (const delayMs of [100, 250, 500]) {
|
|
@@ -2640,54 +2696,72 @@ function readRequestBody(req, maxBytes = 4096) {
|
|
|
2640
2696
|
});
|
|
2641
2697
|
}
|
|
2642
2698
|
|
|
2643
|
-
async function resolveManagerFromPin(supabase, pin, options = {}) {
|
|
2699
|
+
export async function resolveManagerFromPin(supabase, pin, options = {}) {
|
|
2644
2700
|
const normalizedPin = normalizePairingPin(pin);
|
|
2645
2701
|
if (!normalizedPin) {
|
|
2646
2702
|
throw new Error('Enter a 6-digit LiveDesk PIN.');
|
|
2647
2703
|
}
|
|
2648
2704
|
const cacheOwnerKey = hubCacheOwnerForPin(normalizedPin);
|
|
2649
|
-
const
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2705
|
+
const resolved = await resolveManagerWithCachedRecovery(cacheOwnerKey, {
|
|
2706
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
2707
|
+
relayEndpoint: options.relayEndpoint,
|
|
2708
|
+
probeEndpoint: options.probeEndpoint,
|
|
2709
|
+
resolveFreshTarget: async () => {
|
|
2710
|
+
const { data, error } = await supabase.functions.invoke('livedesk-resolve-remote-host', {
|
|
2711
|
+
body: { pin: normalizedPin }
|
|
2712
|
+
});
|
|
2713
|
+
if (error) {
|
|
2714
|
+
throw await normalizePinResolverInvocationError(error);
|
|
2715
|
+
}
|
|
2716
|
+
const result = Array.isArray(data) ? data[0] : data;
|
|
2717
|
+
if (!result?.ok) {
|
|
2718
|
+
const reason = String(result?.reason || 'pin-not-found').trim().toLowerCase();
|
|
2719
|
+
throw createHubDiscoveryError(reason, reason);
|
|
2720
|
+
}
|
|
2721
|
+
if (!result.pair_token) {
|
|
2722
|
+
throw createHubDiscoveryError(
|
|
2723
|
+
'pin-registry-pair-token-missing',
|
|
2724
|
+
'The PIN matched a Hub record without a private connection token.'
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
const endpointCandidates = normalizeEndpointCandidates(result);
|
|
2728
|
+
if (endpointCandidates.length === 0) {
|
|
2729
|
+
throw createHubDiscoveryError(
|
|
2730
|
+
'pin-registry-endpoints-missing',
|
|
2731
|
+
'The PIN matched a Hub record without a reachable address.'
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
const target = await chooseManagerConnectionTarget(endpointCandidates, {
|
|
2735
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
2736
|
+
probeEndpoint: options.probeEndpoint
|
|
2737
|
+
});
|
|
2738
|
+
if (!target) {
|
|
2739
|
+
throw createHubDiscoveryError(
|
|
2740
|
+
'pin-direct-unreachable',
|
|
2741
|
+
`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2744
|
+
return {
|
|
2745
|
+
manager: target.manager,
|
|
2746
|
+
pair: result.pair_token,
|
|
2747
|
+
hubDeviceId: String(result.node_id || '').trim(),
|
|
2748
|
+
endpointCandidates,
|
|
2749
|
+
directReachable: target.directReachable,
|
|
2750
|
+
connectionTransport: target.connectionTransport,
|
|
2751
|
+
discoverySource: 'supabase'
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2674
2754
|
});
|
|
2675
|
-
if (
|
|
2676
|
-
|
|
2755
|
+
if (resolved.discoverySource === 'supabase') {
|
|
2756
|
+
writeCachedHubTarget(cacheOwnerKey, resolved);
|
|
2677
2757
|
}
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
endpointCandidates,
|
|
2684
|
-
directReachable: target.directReachable,
|
|
2685
|
-
connectionTransport: target.connectionTransport,
|
|
2686
|
-
discoverySource: 'supabase'
|
|
2687
|
-
};
|
|
2688
|
-
writeCachedHubTarget(cacheOwnerKey, resolved);
|
|
2758
|
+
console.log(resolved.connectionTransport === 'relay-fallback' && resolved.discoverySource === 'cache'
|
|
2759
|
+
? 'Fresh PIN registry lookup was unavailable; using the saved PIN-bound identity for the bounded encrypted relay path.'
|
|
2760
|
+
: resolved.discoverySource === 'cache'
|
|
2761
|
+
? `Fresh PIN registry lookup was unavailable; using the responding saved direct Hub endpoint ${resolved.manager}.`
|
|
2762
|
+
: `Found current PIN-bound LiveDesk Hub at ${resolved.manager}.`);
|
|
2689
2763
|
return resolved;
|
|
2690
|
-
}
|
|
2764
|
+
}
|
|
2691
2765
|
|
|
2692
2766
|
async function waitForManagerFromSavedPin(supabase, pin, options = {}) {
|
|
2693
2767
|
const configuredIntervalMs = Number(options.intervalMs || 0);
|
|
@@ -2702,7 +2776,8 @@ async function waitForManagerFromSavedPin(supabase, pin, options = {}) {
|
|
|
2702
2776
|
attempts += 1;
|
|
2703
2777
|
try {
|
|
2704
2778
|
return await resolveManagerFromPin(supabase, pin, {
|
|
2705
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
2779
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
2780
|
+
relayEndpoint: options.relayEndpoint
|
|
2706
2781
|
});
|
|
2707
2782
|
} catch (err) {
|
|
2708
2783
|
if (shouldStop()) {
|
|
@@ -2856,9 +2931,11 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
2856
2931
|
}
|
|
2857
2932
|
dashboardState.message = 'Saved LiveDesk PIN found. Waiting for the Hub to become reachable.';
|
|
2858
2933
|
try {
|
|
2859
|
-
const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
|
|
2860
|
-
|
|
2861
|
-
|
|
2934
|
+
const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
|
|
2935
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
2936
|
+
relayEndpoint: options.relayEndpoint,
|
|
2937
|
+
shouldStop: () => completed || dashboardState.loggedOut
|
|
2938
|
+
});
|
|
2862
2939
|
if (!resolved) {
|
|
2863
2940
|
return;
|
|
2864
2941
|
}
|
|
@@ -3088,7 +3165,8 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
3088
3165
|
pin = normalizePairingPin(pin);
|
|
3089
3166
|
try {
|
|
3090
3167
|
const resolved = await resolveManagerFromPin(supabase, pin, {
|
|
3091
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
3168
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3169
|
+
relayEndpoint: options.relayEndpoint
|
|
3092
3170
|
});
|
|
3093
3171
|
writeSavedPin(pin);
|
|
3094
3172
|
applyStartupPreference(pendingStartup, startupArgs);
|
|
@@ -3223,6 +3301,8 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3223
3301
|
deviceId: options.deviceId,
|
|
3224
3302
|
engine: options.engine,
|
|
3225
3303
|
slot: options.slot,
|
|
3304
|
+
assignedHubId: process.env.LIVEDESK_ASSIGNED_HUB_ID,
|
|
3305
|
+
roleVersion: process.env.LIVEDESK_ROLE_VERSION,
|
|
3226
3306
|
savedSession: options.savedSession,
|
|
3227
3307
|
initialChoice: options.initialChoice,
|
|
3228
3308
|
initialChoiceMessage: options.initialChoiceMessage,
|
|
@@ -3251,7 +3331,8 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3251
3331
|
resolvePin: pin => {
|
|
3252
3332
|
if (!supabase) throw new Error('supabase-session-required');
|
|
3253
3333
|
return resolveManagerFromPin(supabase, pin, {
|
|
3254
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
3334
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3335
|
+
relayEndpoint: options.relayEndpoint
|
|
3255
3336
|
});
|
|
3256
3337
|
},
|
|
3257
3338
|
changeRole: async (role, snapshot) => {
|
|
@@ -3350,7 +3431,8 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3350
3431
|
startupArgs: options.startupArgs,
|
|
3351
3432
|
savedSession: options.savedSession,
|
|
3352
3433
|
savedPin: options.savedPin,
|
|
3353
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
3434
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3435
|
+
relayEndpoint: options.relayEndpoint
|
|
3354
3436
|
});
|
|
3355
3437
|
if (options.openBrowser !== false) {
|
|
3356
3438
|
console.log('Opening LiveDesk connection page...');
|
|
@@ -3528,10 +3610,16 @@ export async function chooseManagerConnectionTarget(candidates, options = {}) {
|
|
|
3528
3610
|
}
|
|
3529
3611
|
|
|
3530
3612
|
export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
|
|
3531
|
-
const cached = options
|
|
3613
|
+
const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
|
|
3614
|
+
? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
|
|
3615
|
+
: readCachedHubTarget(ownerKey);
|
|
3532
3616
|
if (!cached) return null;
|
|
3533
|
-
const
|
|
3534
|
-
|
|
3617
|
+
const target = await chooseManagerConnectionTarget(cached.endpointCandidates, {
|
|
3618
|
+
allowRelayFallback: false,
|
|
3619
|
+
probeEndpoint: options.probeEndpoint
|
|
3620
|
+
});
|
|
3621
|
+
if (!target) return null;
|
|
3622
|
+
const manager = target.manager;
|
|
3535
3623
|
const resolved = {
|
|
3536
3624
|
manager,
|
|
3537
3625
|
pair: cached.pair,
|
|
@@ -3540,6 +3628,8 @@ export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
|
|
|
3540
3628
|
manager,
|
|
3541
3629
|
...cached.endpointCandidates.filter(endpoint => endpoint !== manager)
|
|
3542
3630
|
],
|
|
3631
|
+
directReachable: target.directReachable,
|
|
3632
|
+
connectionTransport: target.connectionTransport,
|
|
3543
3633
|
discoverySource: 'cache'
|
|
3544
3634
|
};
|
|
3545
3635
|
if (options.persist !== false) {
|
|
@@ -3548,6 +3638,86 @@ export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
|
|
|
3548
3638
|
return resolved;
|
|
3549
3639
|
}
|
|
3550
3640
|
|
|
3641
|
+
export function resolveManagerFromCachedRelayFallback(ownerKey, options = {}) {
|
|
3642
|
+
if (options.allowRelayFallback !== true) return null;
|
|
3643
|
+
const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
|
|
3644
|
+
? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
|
|
3645
|
+
: readCachedHubTarget(ownerKey);
|
|
3646
|
+
if (!cached) return null;
|
|
3647
|
+
const normalizedOwner = String(cached.ownerKey || '');
|
|
3648
|
+
const accountIdentityComplete = normalizedOwner.startsWith('user:')
|
|
3649
|
+
&& normalizedOwner.length > 'user:'.length
|
|
3650
|
+
&& Boolean(cached.hubDeviceId);
|
|
3651
|
+
// The public PIN resolver intentionally does not expose node_id. Its
|
|
3652
|
+
// previous signed pair/endpoints remain bound to the exact saved six-digit
|
|
3653
|
+
// PIN owner in the private, finite-lifetime cache.
|
|
3654
|
+
const pinIdentityComplete = /^pin:[0-9]{6}$/.test(normalizedOwner);
|
|
3655
|
+
const relayEndpoint = String(options.relayEndpoint || '').trim().replace(/^tcp:\/\//i, '');
|
|
3656
|
+
if ((!accountIdentityComplete && !pinIdentityComplete) || !parseManagerEndpoint(relayEndpoint)) {
|
|
3657
|
+
return null;
|
|
3658
|
+
}
|
|
3659
|
+
return {
|
|
3660
|
+
manager: cached.endpointCandidates[0],
|
|
3661
|
+
pair: cached.pair,
|
|
3662
|
+
hubDeviceId: cached.hubDeviceId,
|
|
3663
|
+
endpointCandidates: [...cached.endpointCandidates],
|
|
3664
|
+
directReachable: false,
|
|
3665
|
+
connectionTransport: 'relay-fallback',
|
|
3666
|
+
discoverySource: 'cache'
|
|
3667
|
+
};
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
export async function resolveManagerWithCachedRecovery(ownerKey, options = {}) {
|
|
3671
|
+
const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
|
|
3672
|
+
? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
|
|
3673
|
+
: readCachedHubTarget(ownerKey);
|
|
3674
|
+
try {
|
|
3675
|
+
if (typeof options.resolveFreshTarget !== 'function') {
|
|
3676
|
+
throw createHubDiscoveryError('fresh-registry-resolver-required', 'Fresh Hub registry resolver is required.');
|
|
3677
|
+
}
|
|
3678
|
+
const configuredFreshTimeoutMs = Number(options.freshTimeoutMs);
|
|
3679
|
+
const freshTimeoutMs = Number.isFinite(configuredFreshTimeoutMs)
|
|
3680
|
+
? Math.max(FRESH_REGISTRY_TIMEOUT_MIN_MS, Math.min(FRESH_REGISTRY_TIMEOUT_MAX_MS, configuredFreshTimeoutMs))
|
|
3681
|
+
: FRESH_REGISTRY_TIMEOUT_MS;
|
|
3682
|
+
let timeoutHandle = null;
|
|
3683
|
+
const fresh = await Promise.race([
|
|
3684
|
+
Promise.resolve().then(() => options.resolveFreshTarget()),
|
|
3685
|
+
new Promise((_, reject) => {
|
|
3686
|
+
timeoutHandle = setTimeout(() => reject(createHubDiscoveryError(
|
|
3687
|
+
'hub-registry-timeout',
|
|
3688
|
+
`Fresh LiveDesk Hub registry lookup exceeded ${freshTimeoutMs}ms.`
|
|
3689
|
+
)), freshTimeoutMs);
|
|
3690
|
+
})
|
|
3691
|
+
]).finally(() => clearTimeout(timeoutHandle));
|
|
3692
|
+
if (!fresh?.manager || !fresh?.pair) {
|
|
3693
|
+
throw createHubDiscoveryError('fresh-registry-target-invalid', 'Fresh Hub registry target is incomplete.');
|
|
3694
|
+
}
|
|
3695
|
+
return {
|
|
3696
|
+
...fresh,
|
|
3697
|
+
discoverySource: fresh.discoverySource || 'supabase'
|
|
3698
|
+
};
|
|
3699
|
+
} catch (error) {
|
|
3700
|
+
if (canUseCachedRelayAfterRegistryError(error)) {
|
|
3701
|
+
const direct = await resolveManagerFromCachedTarget(ownerKey, {
|
|
3702
|
+
cachedTarget: cached,
|
|
3703
|
+
probeEndpoint: options.probeEndpoint,
|
|
3704
|
+
// A TCP listener is not pair-token authentication. Only a
|
|
3705
|
+
// fresh signed registry result may extend cache trust.
|
|
3706
|
+
persist: false
|
|
3707
|
+
});
|
|
3708
|
+
if (direct) return direct;
|
|
3709
|
+
const relay = resolveManagerFromCachedRelayFallback(ownerKey, {
|
|
3710
|
+
cachedTarget: cached,
|
|
3711
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3712
|
+
relayEndpoint: options.relayEndpoint,
|
|
3713
|
+
now: options.now
|
|
3714
|
+
});
|
|
3715
|
+
if (relay) return relay;
|
|
3716
|
+
}
|
|
3717
|
+
throw error;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3551
3721
|
export function getDiscoveryRetryDelay(attempt, configuredIntervalMs = 0, random = Math.random) {
|
|
3552
3722
|
const configured = Number(configuredIntervalMs);
|
|
3553
3723
|
if (Number.isFinite(configured) && configured > 0) {
|
|
@@ -3599,34 +3769,50 @@ function normalizeEndpointCandidates(target) {
|
|
|
3599
3769
|
return [...new Set(rawCandidates.map(value => String(value || '').trim()).filter(Boolean))];
|
|
3600
3770
|
}
|
|
3601
3771
|
|
|
3602
|
-
async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
3772
|
+
export async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
3603
3773
|
await refreshSessionIfNeeded(supabase);
|
|
3604
3774
|
const { data, error } = await supabase
|
|
3605
3775
|
.from('livedesk_remote_host_targets')
|
|
3606
3776
|
.select('node_id, endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
|
|
3607
3777
|
.eq('product_key', 'livedesk')
|
|
3608
3778
|
.maybeSingle();
|
|
3609
|
-
if (error) {
|
|
3610
|
-
throw error;
|
|
3611
|
-
}
|
|
3612
|
-
if (!data?.active) {
|
|
3613
|
-
throw
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
}
|
|
3618
|
-
if (
|
|
3619
|
-
throw
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3779
|
+
if (error) {
|
|
3780
|
+
throw error;
|
|
3781
|
+
}
|
|
3782
|
+
if (!data?.active) {
|
|
3783
|
+
throw createHubDiscoveryError(
|
|
3784
|
+
'hub-registry-missing',
|
|
3785
|
+
'No active LiveDesk Hub was found for this Google account. Start LiveDesk Hub on the screen wall computer and sign in there first.'
|
|
3786
|
+
);
|
|
3787
|
+
}
|
|
3788
|
+
if (data.expires_at && Date.parse(data.expires_at) <= Date.now()) {
|
|
3789
|
+
throw createHubDiscoveryError(
|
|
3790
|
+
'hub-registry-expired',
|
|
3791
|
+
'The LiveDesk Hub record is expired. Open the Hub screen again while signed in.'
|
|
3792
|
+
);
|
|
3793
|
+
}
|
|
3794
|
+
if (!data.pair_token) {
|
|
3795
|
+
throw createHubDiscoveryError(
|
|
3796
|
+
'hub-registry-pair-token-missing',
|
|
3797
|
+
'The LiveDesk Hub record is missing its private connection token.'
|
|
3798
|
+
);
|
|
3799
|
+
}
|
|
3800
|
+
const endpointCandidates = normalizeEndpointCandidates(data);
|
|
3801
|
+
if (endpointCandidates.length === 0) {
|
|
3802
|
+
throw createHubDiscoveryError(
|
|
3803
|
+
'hub-registry-endpoints-missing',
|
|
3804
|
+
'The LiveDesk Hub record does not contain a reachable local address.'
|
|
3805
|
+
);
|
|
3806
|
+
}
|
|
3625
3807
|
const target = await chooseManagerConnectionTarget(endpointCandidates, {
|
|
3626
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
3808
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3809
|
+
probeEndpoint: options.probeEndpoint
|
|
3627
3810
|
});
|
|
3628
3811
|
if (!target) {
|
|
3629
|
-
throw
|
|
3812
|
+
throw createHubDiscoveryError(
|
|
3813
|
+
'hub-direct-unreachable',
|
|
3814
|
+
`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`
|
|
3815
|
+
);
|
|
3630
3816
|
}
|
|
3631
3817
|
return {
|
|
3632
3818
|
manager: target.manager,
|
|
@@ -3669,6 +3855,7 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
3669
3855
|
let attempts = 0;
|
|
3670
3856
|
let lastMessage = '';
|
|
3671
3857
|
let wakeListener = null;
|
|
3858
|
+
let initialError = options.initialError || null;
|
|
3672
3859
|
console.log('Waiting for a LiveDesk Hub. LiveDesk is listening for Hub-online events with adaptive registry retries as a fallback.');
|
|
3673
3860
|
try {
|
|
3674
3861
|
while (true) {
|
|
@@ -3677,6 +3864,11 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
3677
3864
|
}
|
|
3678
3865
|
attempts += 1;
|
|
3679
3866
|
try {
|
|
3867
|
+
if (initialError) {
|
|
3868
|
+
const error = initialError;
|
|
3869
|
+
initialError = null;
|
|
3870
|
+
throw error;
|
|
3871
|
+
}
|
|
3680
3872
|
return await resolveManagerFromSupabase(supabase, {
|
|
3681
3873
|
allowRelayFallback: options.allowRelayFallback === true
|
|
3682
3874
|
});
|
|
@@ -3752,7 +3944,8 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3752
3944
|
choice = { type: 'google', session: savedSession };
|
|
3753
3945
|
} else if ((parsed.startupRun || existingConnectionPage) && savedPin) {
|
|
3754
3946
|
const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
|
|
3755
|
-
allowRelayFallback
|
|
3947
|
+
allowRelayFallback,
|
|
3948
|
+
relayEndpoint: parsed.relay
|
|
3756
3949
|
});
|
|
3757
3950
|
choice = { type: 'pin', ...resolved };
|
|
3758
3951
|
} else if (existingConnectionPage && previousChoice) {
|
|
@@ -3769,8 +3962,9 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3769
3962
|
savedSession,
|
|
3770
3963
|
savedPin,
|
|
3771
3964
|
allowRelayFallback,
|
|
3965
|
+
relayEndpoint: parsed.relay,
|
|
3772
3966
|
openBrowser: parsed.openBrowserOnStart
|
|
3773
|
-
});
|
|
3967
|
+
});
|
|
3774
3968
|
}
|
|
3775
3969
|
connectionPage = existingConnectionPage || choice.connectionPage || null;
|
|
3776
3970
|
if (choice.type === 'pin') {
|
|
@@ -3792,26 +3986,39 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3792
3986
|
: 'Connected by LiveDesk PIN.');
|
|
3793
3987
|
} else {
|
|
3794
3988
|
const cacheOwnerKey = hubCacheOwnerForSession(choice.session);
|
|
3795
|
-
let resolved = await resolveManagerFromCachedTarget(cacheOwnerKey);
|
|
3796
3989
|
let session = choice.session;
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3990
|
+
let initialDiscoveryError = null;
|
|
3991
|
+
let resolved = null;
|
|
3992
|
+
try {
|
|
3993
|
+
resolved = await resolveManagerWithCachedRecovery(cacheOwnerKey, {
|
|
3994
|
+
allowRelayFallback,
|
|
3995
|
+
relayEndpoint: parsed.relay,
|
|
3996
|
+
resolveFreshTarget: async () => {
|
|
3997
|
+
// The signed registry is authoritative for Hub replacement
|
|
3998
|
+
// and pair-token rotation. Cache direct/relay recovery is
|
|
3999
|
+
// considered only after this one current lookup reports
|
|
4000
|
+
// an explicitly recoverable availability failure.
|
|
4001
|
+
try {
|
|
4002
|
+
session = await activateSupabaseSession(supabase, choice.session);
|
|
4003
|
+
} catch (error) {
|
|
4004
|
+
if (!isRefreshTokenAlreadyUsedError(error)) throw error;
|
|
4005
|
+
session = readSavedSessionFromFile() || choice.session;
|
|
4006
|
+
console.warn('[LiveDesk Client] Another auth owner rotated the refresh token. Keeping the runtime alive while the saved session catches up.');
|
|
4007
|
+
}
|
|
4008
|
+
return await resolveManagerFromSupabase(supabase, {
|
|
4009
|
+
allowRelayFallback
|
|
4010
|
+
});
|
|
4011
|
+
}
|
|
4012
|
+
});
|
|
4013
|
+
} catch (error) {
|
|
4014
|
+
initialDiscoveryError = error;
|
|
4015
|
+
}
|
|
4016
|
+
discoverySource = resolved?.discoverySource || 'supabase';
|
|
4017
|
+
if (resolved?.discoverySource === 'cache') {
|
|
3801
4018
|
writeSavedSessionToFile(session);
|
|
3802
|
-
console.log(
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
// Activate the accepted browser session only when registry
|
|
3806
|
-
// discovery is needed; a reachable account-bound cache can
|
|
3807
|
-
// start the Agent without a Supabase round trip.
|
|
3808
|
-
try {
|
|
3809
|
-
session = await activateSupabaseSession(supabase, choice.session);
|
|
3810
|
-
} catch (error) {
|
|
3811
|
-
if (!isRefreshTokenAlreadyUsedError(error)) throw error;
|
|
3812
|
-
session = readSavedSessionFromFile() || choice.session;
|
|
3813
|
-
console.warn('[LiveDesk Client] Another auth owner rotated the refresh token. Keeping the runtime alive while the saved session catches up.');
|
|
3814
|
-
}
|
|
4019
|
+
console.log(resolved.connectionTransport === 'relay-fallback'
|
|
4020
|
+
? `Fresh Hub registry lookup was unavailable; using saved account-bound Hub identity ${resolved.hubDeviceId} for the bounded encrypted relay path.`
|
|
4021
|
+
: `Fresh Hub registry lookup was unavailable; using the responding saved direct Hub endpoint ${resolved.manager}.`);
|
|
3815
4022
|
}
|
|
3816
4023
|
choice.session = session;
|
|
3817
4024
|
const email = session?.user?.email ? ` as ${session.user.email}` : '';
|
|
@@ -3819,18 +4026,20 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3819
4026
|
if (!resolved) {
|
|
3820
4027
|
resolved = await waitForManagerFromSupabase(supabase, {
|
|
3821
4028
|
allowRelayFallback,
|
|
4029
|
+
initialError: initialDiscoveryError,
|
|
3822
4030
|
shouldStop: () => Boolean(roleRestartRequest?.role)
|
|
3823
4031
|
});
|
|
4032
|
+
discoverySource = 'supabase';
|
|
3824
4033
|
}
|
|
3825
4034
|
if (!resolved && roleRestartRequest?.role) {
|
|
3826
|
-
return {
|
|
3827
|
-
...parsed,
|
|
3828
|
-
manager,
|
|
3829
|
-
pair,
|
|
3830
|
-
slot: normalizeSlotNumber(parsed.slot),
|
|
3831
|
-
connectionPage,
|
|
3832
|
-
forwarded,
|
|
3833
|
-
rediscoverOnDisconnect: shouldLogin,
|
|
4035
|
+
return {
|
|
4036
|
+
...parsed,
|
|
4037
|
+
manager,
|
|
4038
|
+
pair,
|
|
4039
|
+
slot: normalizeSlotNumber(parsed.slot),
|
|
4040
|
+
connectionPage,
|
|
4041
|
+
forwarded,
|
|
4042
|
+
rediscoverOnDisconnect: shouldLogin,
|
|
3834
4043
|
rediscoverOnInvalidPair: shouldLogin
|
|
3835
4044
|
};
|
|
3836
4045
|
}
|
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.206",
|
|
4
4
|
"description": "LiveDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"ws": "^8.18.3"
|
|
41
41
|
},
|
|
42
42
|
"optionalDependencies": {
|
|
43
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
44
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
45
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
46
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
43
|
+
"@livedesk/fast-linux-x64": "0.1.412",
|
|
44
|
+
"@livedesk/fast-osx-arm64": "0.1.412",
|
|
45
|
+
"@livedesk/fast-osx-x64": "0.1.412",
|
|
46
|
+
"@livedesk/fast-win-x64": "0.1.412"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|