livedesk 0.1.449 → 0.1.450
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/client/bin/livedesk-client.js +312 -105
- package/client/package.json +5 -5
- package/hub/package.json +1 -1
- 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-CDA-kp_V.js → index-vBxXkd_R.js} +12 -12
- package/web/dist/index.html +1 -1
|
@@ -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);
|
|
@@ -3251,7 +3329,8 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3251
3329
|
resolvePin: pin => {
|
|
3252
3330
|
if (!supabase) throw new Error('supabase-session-required');
|
|
3253
3331
|
return resolveManagerFromPin(supabase, pin, {
|
|
3254
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
3332
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3333
|
+
relayEndpoint: options.relayEndpoint
|
|
3255
3334
|
});
|
|
3256
3335
|
},
|
|
3257
3336
|
changeRole: async (role, snapshot) => {
|
|
@@ -3350,7 +3429,8 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3350
3429
|
startupArgs: options.startupArgs,
|
|
3351
3430
|
savedSession: options.savedSession,
|
|
3352
3431
|
savedPin: options.savedPin,
|
|
3353
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
3432
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3433
|
+
relayEndpoint: options.relayEndpoint
|
|
3354
3434
|
});
|
|
3355
3435
|
if (options.openBrowser !== false) {
|
|
3356
3436
|
console.log('Opening LiveDesk connection page...');
|
|
@@ -3528,10 +3608,16 @@ export async function chooseManagerConnectionTarget(candidates, options = {}) {
|
|
|
3528
3608
|
}
|
|
3529
3609
|
|
|
3530
3610
|
export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
|
|
3531
|
-
const cached = options
|
|
3611
|
+
const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
|
|
3612
|
+
? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
|
|
3613
|
+
: readCachedHubTarget(ownerKey);
|
|
3532
3614
|
if (!cached) return null;
|
|
3533
|
-
const
|
|
3534
|
-
|
|
3615
|
+
const target = await chooseManagerConnectionTarget(cached.endpointCandidates, {
|
|
3616
|
+
allowRelayFallback: false,
|
|
3617
|
+
probeEndpoint: options.probeEndpoint
|
|
3618
|
+
});
|
|
3619
|
+
if (!target) return null;
|
|
3620
|
+
const manager = target.manager;
|
|
3535
3621
|
const resolved = {
|
|
3536
3622
|
manager,
|
|
3537
3623
|
pair: cached.pair,
|
|
@@ -3540,6 +3626,8 @@ export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
|
|
|
3540
3626
|
manager,
|
|
3541
3627
|
...cached.endpointCandidates.filter(endpoint => endpoint !== manager)
|
|
3542
3628
|
],
|
|
3629
|
+
directReachable: target.directReachable,
|
|
3630
|
+
connectionTransport: target.connectionTransport,
|
|
3543
3631
|
discoverySource: 'cache'
|
|
3544
3632
|
};
|
|
3545
3633
|
if (options.persist !== false) {
|
|
@@ -3548,6 +3636,86 @@ export async function resolveManagerFromCachedTarget(ownerKey, options = {}) {
|
|
|
3548
3636
|
return resolved;
|
|
3549
3637
|
}
|
|
3550
3638
|
|
|
3639
|
+
export function resolveManagerFromCachedRelayFallback(ownerKey, options = {}) {
|
|
3640
|
+
if (options.allowRelayFallback !== true) return null;
|
|
3641
|
+
const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
|
|
3642
|
+
? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
|
|
3643
|
+
: readCachedHubTarget(ownerKey);
|
|
3644
|
+
if (!cached) return null;
|
|
3645
|
+
const normalizedOwner = String(cached.ownerKey || '');
|
|
3646
|
+
const accountIdentityComplete = normalizedOwner.startsWith('user:')
|
|
3647
|
+
&& normalizedOwner.length > 'user:'.length
|
|
3648
|
+
&& Boolean(cached.hubDeviceId);
|
|
3649
|
+
// The public PIN resolver intentionally does not expose node_id. Its
|
|
3650
|
+
// previous signed pair/endpoints remain bound to the exact saved six-digit
|
|
3651
|
+
// PIN owner in the private, finite-lifetime cache.
|
|
3652
|
+
const pinIdentityComplete = /^pin:[0-9]{6}$/.test(normalizedOwner);
|
|
3653
|
+
const relayEndpoint = String(options.relayEndpoint || '').trim().replace(/^tcp:\/\//i, '');
|
|
3654
|
+
if ((!accountIdentityComplete && !pinIdentityComplete) || !parseManagerEndpoint(relayEndpoint)) {
|
|
3655
|
+
return null;
|
|
3656
|
+
}
|
|
3657
|
+
return {
|
|
3658
|
+
manager: cached.endpointCandidates[0],
|
|
3659
|
+
pair: cached.pair,
|
|
3660
|
+
hubDeviceId: cached.hubDeviceId,
|
|
3661
|
+
endpointCandidates: [...cached.endpointCandidates],
|
|
3662
|
+
directReachable: false,
|
|
3663
|
+
connectionTransport: 'relay-fallback',
|
|
3664
|
+
discoverySource: 'cache'
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
export async function resolveManagerWithCachedRecovery(ownerKey, options = {}) {
|
|
3669
|
+
const cached = Object.prototype.hasOwnProperty.call(options, 'cachedTarget')
|
|
3670
|
+
? normalizeCachedHubTarget(options.cachedTarget, ownerKey, options.now)
|
|
3671
|
+
: readCachedHubTarget(ownerKey);
|
|
3672
|
+
try {
|
|
3673
|
+
if (typeof options.resolveFreshTarget !== 'function') {
|
|
3674
|
+
throw createHubDiscoveryError('fresh-registry-resolver-required', 'Fresh Hub registry resolver is required.');
|
|
3675
|
+
}
|
|
3676
|
+
const configuredFreshTimeoutMs = Number(options.freshTimeoutMs);
|
|
3677
|
+
const freshTimeoutMs = Number.isFinite(configuredFreshTimeoutMs)
|
|
3678
|
+
? Math.max(FRESH_REGISTRY_TIMEOUT_MIN_MS, Math.min(FRESH_REGISTRY_TIMEOUT_MAX_MS, configuredFreshTimeoutMs))
|
|
3679
|
+
: FRESH_REGISTRY_TIMEOUT_MS;
|
|
3680
|
+
let timeoutHandle = null;
|
|
3681
|
+
const fresh = await Promise.race([
|
|
3682
|
+
Promise.resolve().then(() => options.resolveFreshTarget()),
|
|
3683
|
+
new Promise((_, reject) => {
|
|
3684
|
+
timeoutHandle = setTimeout(() => reject(createHubDiscoveryError(
|
|
3685
|
+
'hub-registry-timeout',
|
|
3686
|
+
`Fresh LiveDesk Hub registry lookup exceeded ${freshTimeoutMs}ms.`
|
|
3687
|
+
)), freshTimeoutMs);
|
|
3688
|
+
})
|
|
3689
|
+
]).finally(() => clearTimeout(timeoutHandle));
|
|
3690
|
+
if (!fresh?.manager || !fresh?.pair) {
|
|
3691
|
+
throw createHubDiscoveryError('fresh-registry-target-invalid', 'Fresh Hub registry target is incomplete.');
|
|
3692
|
+
}
|
|
3693
|
+
return {
|
|
3694
|
+
...fresh,
|
|
3695
|
+
discoverySource: fresh.discoverySource || 'supabase'
|
|
3696
|
+
};
|
|
3697
|
+
} catch (error) {
|
|
3698
|
+
if (canUseCachedRelayAfterRegistryError(error)) {
|
|
3699
|
+
const direct = await resolveManagerFromCachedTarget(ownerKey, {
|
|
3700
|
+
cachedTarget: cached,
|
|
3701
|
+
probeEndpoint: options.probeEndpoint,
|
|
3702
|
+
// A TCP listener is not pair-token authentication. Only a
|
|
3703
|
+
// fresh signed registry result may extend cache trust.
|
|
3704
|
+
persist: false
|
|
3705
|
+
});
|
|
3706
|
+
if (direct) return direct;
|
|
3707
|
+
const relay = resolveManagerFromCachedRelayFallback(ownerKey, {
|
|
3708
|
+
cachedTarget: cached,
|
|
3709
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3710
|
+
relayEndpoint: options.relayEndpoint,
|
|
3711
|
+
now: options.now
|
|
3712
|
+
});
|
|
3713
|
+
if (relay) return relay;
|
|
3714
|
+
}
|
|
3715
|
+
throw error;
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3551
3719
|
export function getDiscoveryRetryDelay(attempt, configuredIntervalMs = 0, random = Math.random) {
|
|
3552
3720
|
const configured = Number(configuredIntervalMs);
|
|
3553
3721
|
if (Number.isFinite(configured) && configured > 0) {
|
|
@@ -3599,34 +3767,50 @@ function normalizeEndpointCandidates(target) {
|
|
|
3599
3767
|
return [...new Set(rawCandidates.map(value => String(value || '').trim()).filter(Boolean))];
|
|
3600
3768
|
}
|
|
3601
3769
|
|
|
3602
|
-
async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
3770
|
+
export async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
3603
3771
|
await refreshSessionIfNeeded(supabase);
|
|
3604
3772
|
const { data, error } = await supabase
|
|
3605
3773
|
.from('livedesk_remote_host_targets')
|
|
3606
3774
|
.select('node_id, endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
|
|
3607
3775
|
.eq('product_key', 'livedesk')
|
|
3608
3776
|
.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
|
-
|
|
3777
|
+
if (error) {
|
|
3778
|
+
throw error;
|
|
3779
|
+
}
|
|
3780
|
+
if (!data?.active) {
|
|
3781
|
+
throw createHubDiscoveryError(
|
|
3782
|
+
'hub-registry-missing',
|
|
3783
|
+
'No active LiveDesk Hub was found for this Google account. Start LiveDesk Hub on the screen wall computer and sign in there first.'
|
|
3784
|
+
);
|
|
3785
|
+
}
|
|
3786
|
+
if (data.expires_at && Date.parse(data.expires_at) <= Date.now()) {
|
|
3787
|
+
throw createHubDiscoveryError(
|
|
3788
|
+
'hub-registry-expired',
|
|
3789
|
+
'The LiveDesk Hub record is expired. Open the Hub screen again while signed in.'
|
|
3790
|
+
);
|
|
3791
|
+
}
|
|
3792
|
+
if (!data.pair_token) {
|
|
3793
|
+
throw createHubDiscoveryError(
|
|
3794
|
+
'hub-registry-pair-token-missing',
|
|
3795
|
+
'The LiveDesk Hub record is missing its private connection token.'
|
|
3796
|
+
);
|
|
3797
|
+
}
|
|
3798
|
+
const endpointCandidates = normalizeEndpointCandidates(data);
|
|
3799
|
+
if (endpointCandidates.length === 0) {
|
|
3800
|
+
throw createHubDiscoveryError(
|
|
3801
|
+
'hub-registry-endpoints-missing',
|
|
3802
|
+
'The LiveDesk Hub record does not contain a reachable local address.'
|
|
3803
|
+
);
|
|
3804
|
+
}
|
|
3625
3805
|
const target = await chooseManagerConnectionTarget(endpointCandidates, {
|
|
3626
|
-
allowRelayFallback: options.allowRelayFallback === true
|
|
3806
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
3807
|
+
probeEndpoint: options.probeEndpoint
|
|
3627
3808
|
});
|
|
3628
3809
|
if (!target) {
|
|
3629
|
-
throw
|
|
3810
|
+
throw createHubDiscoveryError(
|
|
3811
|
+
'hub-direct-unreachable',
|
|
3812
|
+
`No reachable LiveDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`
|
|
3813
|
+
);
|
|
3630
3814
|
}
|
|
3631
3815
|
return {
|
|
3632
3816
|
manager: target.manager,
|
|
@@ -3669,6 +3853,7 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
3669
3853
|
let attempts = 0;
|
|
3670
3854
|
let lastMessage = '';
|
|
3671
3855
|
let wakeListener = null;
|
|
3856
|
+
let initialError = options.initialError || null;
|
|
3672
3857
|
console.log('Waiting for a LiveDesk Hub. LiveDesk is listening for Hub-online events with adaptive registry retries as a fallback.');
|
|
3673
3858
|
try {
|
|
3674
3859
|
while (true) {
|
|
@@ -3677,6 +3862,11 @@ async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
3677
3862
|
}
|
|
3678
3863
|
attempts += 1;
|
|
3679
3864
|
try {
|
|
3865
|
+
if (initialError) {
|
|
3866
|
+
const error = initialError;
|
|
3867
|
+
initialError = null;
|
|
3868
|
+
throw error;
|
|
3869
|
+
}
|
|
3680
3870
|
return await resolveManagerFromSupabase(supabase, {
|
|
3681
3871
|
allowRelayFallback: options.allowRelayFallback === true
|
|
3682
3872
|
});
|
|
@@ -3752,7 +3942,8 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3752
3942
|
choice = { type: 'google', session: savedSession };
|
|
3753
3943
|
} else if ((parsed.startupRun || existingConnectionPage) && savedPin) {
|
|
3754
3944
|
const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
|
|
3755
|
-
allowRelayFallback
|
|
3945
|
+
allowRelayFallback,
|
|
3946
|
+
relayEndpoint: parsed.relay
|
|
3756
3947
|
});
|
|
3757
3948
|
choice = { type: 'pin', ...resolved };
|
|
3758
3949
|
} else if (existingConnectionPage && previousChoice) {
|
|
@@ -3769,8 +3960,9 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3769
3960
|
savedSession,
|
|
3770
3961
|
savedPin,
|
|
3771
3962
|
allowRelayFallback,
|
|
3963
|
+
relayEndpoint: parsed.relay,
|
|
3772
3964
|
openBrowser: parsed.openBrowserOnStart
|
|
3773
|
-
});
|
|
3965
|
+
});
|
|
3774
3966
|
}
|
|
3775
3967
|
connectionPage = existingConnectionPage || choice.connectionPage || null;
|
|
3776
3968
|
if (choice.type === 'pin') {
|
|
@@ -3792,26 +3984,39 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3792
3984
|
: 'Connected by LiveDesk PIN.');
|
|
3793
3985
|
} else {
|
|
3794
3986
|
const cacheOwnerKey = hubCacheOwnerForSession(choice.session);
|
|
3795
|
-
let resolved = await resolveManagerFromCachedTarget(cacheOwnerKey);
|
|
3796
3987
|
let session = choice.session;
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3988
|
+
let initialDiscoveryError = null;
|
|
3989
|
+
let resolved = null;
|
|
3990
|
+
try {
|
|
3991
|
+
resolved = await resolveManagerWithCachedRecovery(cacheOwnerKey, {
|
|
3992
|
+
allowRelayFallback,
|
|
3993
|
+
relayEndpoint: parsed.relay,
|
|
3994
|
+
resolveFreshTarget: async () => {
|
|
3995
|
+
// The signed registry is authoritative for Hub replacement
|
|
3996
|
+
// and pair-token rotation. Cache direct/relay recovery is
|
|
3997
|
+
// considered only after this one current lookup reports
|
|
3998
|
+
// an explicitly recoverable availability failure.
|
|
3999
|
+
try {
|
|
4000
|
+
session = await activateSupabaseSession(supabase, choice.session);
|
|
4001
|
+
} catch (error) {
|
|
4002
|
+
if (!isRefreshTokenAlreadyUsedError(error)) throw error;
|
|
4003
|
+
session = readSavedSessionFromFile() || choice.session;
|
|
4004
|
+
console.warn('[LiveDesk Client] Another auth owner rotated the refresh token. Keeping the runtime alive while the saved session catches up.');
|
|
4005
|
+
}
|
|
4006
|
+
return await resolveManagerFromSupabase(supabase, {
|
|
4007
|
+
allowRelayFallback
|
|
4008
|
+
});
|
|
4009
|
+
}
|
|
4010
|
+
});
|
|
4011
|
+
} catch (error) {
|
|
4012
|
+
initialDiscoveryError = error;
|
|
4013
|
+
}
|
|
4014
|
+
discoverySource = resolved?.discoverySource || 'supabase';
|
|
4015
|
+
if (resolved?.discoverySource === 'cache') {
|
|
3801
4016
|
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
|
-
}
|
|
4017
|
+
console.log(resolved.connectionTransport === 'relay-fallback'
|
|
4018
|
+
? `Fresh Hub registry lookup was unavailable; using saved account-bound Hub identity ${resolved.hubDeviceId} for the bounded encrypted relay path.`
|
|
4019
|
+
: `Fresh Hub registry lookup was unavailable; using the responding saved direct Hub endpoint ${resolved.manager}.`);
|
|
3815
4020
|
}
|
|
3816
4021
|
choice.session = session;
|
|
3817
4022
|
const email = session?.user?.email ? ` as ${session.user.email}` : '';
|
|
@@ -3819,18 +4024,20 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null) {
|
|
|
3819
4024
|
if (!resolved) {
|
|
3820
4025
|
resolved = await waitForManagerFromSupabase(supabase, {
|
|
3821
4026
|
allowRelayFallback,
|
|
4027
|
+
initialError: initialDiscoveryError,
|
|
3822
4028
|
shouldStop: () => Boolean(roleRestartRequest?.role)
|
|
3823
4029
|
});
|
|
4030
|
+
discoverySource = 'supabase';
|
|
3824
4031
|
}
|
|
3825
4032
|
if (!resolved && roleRestartRequest?.role) {
|
|
3826
|
-
return {
|
|
3827
|
-
...parsed,
|
|
3828
|
-
manager,
|
|
3829
|
-
pair,
|
|
3830
|
-
slot: normalizeSlotNumber(parsed.slot),
|
|
3831
|
-
connectionPage,
|
|
3832
|
-
forwarded,
|
|
3833
|
-
rediscoverOnDisconnect: shouldLogin,
|
|
4033
|
+
return {
|
|
4034
|
+
...parsed,
|
|
4035
|
+
manager,
|
|
4036
|
+
pair,
|
|
4037
|
+
slot: normalizeSlotNumber(parsed.slot),
|
|
4038
|
+
connectionPage,
|
|
4039
|
+
forwarded,
|
|
4040
|
+
rediscoverOnDisconnect: shouldLogin,
|
|
3834
4041
|
rediscoverOnInvalidPair: shouldLogin
|
|
3835
4042
|
};
|
|
3836
4043
|
}
|
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.205",
|
|
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"
|
package/hub/package.json
CHANGED
package/hub/src/remote-hub.js
CHANGED
|
@@ -4232,9 +4232,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4232
4232
|
const captureTimingAvailable = hasFrameCaptureTiming(message);
|
|
4233
4233
|
const videoTransport = transport === 'udp-p2p'
|
|
4234
4234
|
? 'p2p-udp'
|
|
4235
|
-
: transport === '
|
|
4236
|
-
? '
|
|
4237
|
-
: '
|
|
4235
|
+
: transport === 'relay-binary'
|
|
4236
|
+
? 'encrypted-relay'
|
|
4237
|
+
: transport === 'ws-binary'
|
|
4238
|
+
? 'direct-ws'
|
|
4239
|
+
: 'direct-tcp';
|
|
4238
4240
|
device.latestLiveFrame = {
|
|
4239
4241
|
streamId,
|
|
4240
4242
|
frameSeq,
|
|
@@ -4322,9 +4324,16 @@ export function createRemoteHub(options = {}) {
|
|
|
4322
4324
|
deltaRefreshTileCount: Number.isFinite(Number(message.deltaRefreshTileCount)) ? Number(message.deltaRefreshTileCount) : 0,
|
|
4323
4325
|
deltaTotalTiles: Number.isFinite(Number(message.deltaTotalTiles)) ? Number(message.deltaTotalTiles) : 0,
|
|
4324
4326
|
videoTransport,
|
|
4325
|
-
frameTransportActive: safeString(message.frameTransportActive, 20)
|
|
4327
|
+
frameTransportActive: safeString(message.frameTransportActive, 20)
|
|
4328
|
+
|| (videoTransport === 'p2p-udp' ? 'p2p' : videoTransport === 'encrypted-relay' ? 'relay' : 'direct'),
|
|
4326
4329
|
frameTransportProtocol: safeString(message.frameTransportProtocol, 20)
|
|
4327
|
-
|| (videoTransport === 'p2p-udp'
|
|
4330
|
+
|| (videoTransport === 'p2p-udp'
|
|
4331
|
+
? 'udp'
|
|
4332
|
+
: videoTransport === 'encrypted-relay'
|
|
4333
|
+
? 'relay'
|
|
4334
|
+
: videoTransport === 'direct-ws'
|
|
4335
|
+
? 'ws'
|
|
4336
|
+
: 'tcp'),
|
|
4328
4337
|
frameTransportState: safeString(message.frameTransportState, 20) || 'active',
|
|
4329
4338
|
frameTransportP2pReady: message.frameTransportP2pReady === true,
|
|
4330
4339
|
frameTransportReason: safeString(message.frameTransportReason, 240),
|
|
@@ -4518,7 +4527,11 @@ export function createRemoteHub(options = {}) {
|
|
|
4518
4527
|
}
|
|
4519
4528
|
|
|
4520
4529
|
const frameKind = safeString(header.frameKind || header.kind || header.frameType || '', 40).toLowerCase();
|
|
4521
|
-
const binaryTransport =
|
|
4530
|
+
const binaryTransport = socket.__liveDeskRelayControl === true
|
|
4531
|
+
? 'relay-binary'
|
|
4532
|
+
: state.binaryTransport === 'ws-binary'
|
|
4533
|
+
? 'ws-binary'
|
|
4534
|
+
: 'binary';
|
|
4522
4535
|
if (frameKind === 'thumbnail' || header.type === 'thumbnail.binary') {
|
|
4523
4536
|
applyThumbnailFrame(device, header, framePayload, binaryTransport);
|
|
4524
4537
|
return;
|