@livedesk/client 0.1.225 → 0.1.227
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-client.js +153 -49
- package/package.json +1 -1
- package/src/runtime/client-runtime-server.js +10 -2
package/bin/livedesk-client.js
CHANGED
|
@@ -90,6 +90,7 @@ let agentRestartRequest = null;
|
|
|
90
90
|
let linuxVideoAccelerationStatus = null;
|
|
91
91
|
let discoveryWakeController = new AbortController();
|
|
92
92
|
let networkChangeMonitor = null;
|
|
93
|
+
let deviceRoleMutationTail = Promise.resolve();
|
|
93
94
|
const sessionRefreshesInFlight = new WeakMap();
|
|
94
95
|
const disposeAgentTerminationHandlers = installAgentTerminationHandlers({
|
|
95
96
|
getAgentProcess: () => activeAgentProcess
|
|
@@ -177,6 +178,17 @@ export function requestLocalDiscoveryWake(reason = 'local-trigger') {
|
|
|
177
178
|
previous.abort(String(reason || 'local-trigger'));
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
export function enqueueDeviceRoleMutation(operation) {
|
|
182
|
+
if (typeof operation !== 'function') {
|
|
183
|
+
return Promise.reject(new TypeError('device-role-mutation-operation-required'));
|
|
184
|
+
}
|
|
185
|
+
const pending = deviceRoleMutationTail
|
|
186
|
+
.catch(() => undefined)
|
|
187
|
+
.then(operation);
|
|
188
|
+
deviceRoleMutationTail = pending.catch(() => undefined);
|
|
189
|
+
return pending;
|
|
190
|
+
}
|
|
191
|
+
|
|
180
192
|
function networkInterfaceSignature() {
|
|
181
193
|
return Object.entries(os.networkInterfaces())
|
|
182
194
|
.flatMap(([name, entries]) => (entries || [])
|
|
@@ -1204,12 +1216,18 @@ function isTransientNetworkError(error) {
|
|
|
1204
1216
|
return /fetch failed|network|dns|socket|connection|timeout|getaddrinfo/i.test(getNestedErrorMessage(error));
|
|
1205
1217
|
}
|
|
1206
1218
|
|
|
1207
|
-
function formatDiscoveryError(error) {
|
|
1219
|
+
export function formatDiscoveryError(error) {
|
|
1208
1220
|
if (isTransientNetworkError(error)) {
|
|
1209
1221
|
const code = getNestedErrorCode(error);
|
|
1210
1222
|
return `Network is not ready yet${code ? ` (${code})` : ''}. Waiting for DNS/Wi-Fi after sleep.`;
|
|
1211
1223
|
}
|
|
1212
|
-
|
|
1224
|
+
const message = getNestedErrorMessage(error);
|
|
1225
|
+
if (message) return message;
|
|
1226
|
+
for (const key of ['error_description', 'reason', 'details', 'hint', 'error', 'code']) {
|
|
1227
|
+
const value = error?.[key];
|
|
1228
|
+
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
1229
|
+
}
|
|
1230
|
+
return 'LiveDesk could not read the current Hub registration yet.';
|
|
1213
1231
|
}
|
|
1214
1232
|
|
|
1215
1233
|
function createHubDiscoveryError(code, message, cause = null) {
|
|
@@ -1308,10 +1326,13 @@ async function refreshSessionIfNeededCore(supabase) {
|
|
|
1308
1326
|
return null;
|
|
1309
1327
|
}
|
|
1310
1328
|
const session = { ...candidate, ...normalized.session };
|
|
1311
|
-
const expiresAt = Number(session.expires_at || 0);
|
|
1312
|
-
if (expiresAt <= 0 || expiresAt - Math.floor(Date.now() / 1000) > SESSION_REFRESH_SKEW_SECONDS) {
|
|
1313
|
-
|
|
1314
|
-
|
|
1329
|
+
const expiresAt = Number(session.expires_at || 0);
|
|
1330
|
+
if (expiresAt <= 0 || expiresAt - Math.floor(Date.now() / 1000) > SESSION_REFRESH_SKEW_SECONDS) {
|
|
1331
|
+
// A persisted session is not automatically installed into this
|
|
1332
|
+
// memory-only Supabase client after a process restart. Returning the
|
|
1333
|
+
// file value without setSession makes the UI say "Signed in" while
|
|
1334
|
+
// PostgREST still sends the publishable-key request as anon.
|
|
1335
|
+
return activateSupabaseSession(supabase, session);
|
|
1315
1336
|
}
|
|
1316
1337
|
const { data, error } = await supabase.auth.refreshSession(session);
|
|
1317
1338
|
if (error) {
|
|
@@ -1387,6 +1408,27 @@ async function activateSupabaseSession(supabase, session) {
|
|
|
1387
1408
|
}
|
|
1388
1409
|
return activeSession;
|
|
1389
1410
|
}
|
|
1411
|
+
|
|
1412
|
+
export async function activateRoleChangeSession(supabase, currentSession = null) {
|
|
1413
|
+
if (!supabase?.auth) {
|
|
1414
|
+
throw new Error('Google sign-in is temporarily unavailable. Try again in a moment.');
|
|
1415
|
+
}
|
|
1416
|
+
const current = normalizeRuntimeAuthSession(currentSession, { requireRefreshToken: true });
|
|
1417
|
+
let session = current.ok ? current.session : await refreshSessionIfNeeded(supabase);
|
|
1418
|
+
if (!session?.access_token) {
|
|
1419
|
+
throw new Error('Sign in again before switching this computer to Hub.');
|
|
1420
|
+
}
|
|
1421
|
+
try {
|
|
1422
|
+
return await activateSupabaseSession(supabase, session);
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
if (!isRefreshTokenAlreadyUsedError(error)) throw error;
|
|
1425
|
+
session = await refreshSessionIfNeeded(supabase);
|
|
1426
|
+
if (!session?.access_token) {
|
|
1427
|
+
throw new Error('Sign in again before switching this computer to Hub.');
|
|
1428
|
+
}
|
|
1429
|
+
return activateSupabaseSession(supabase, session);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1390
1432
|
|
|
1391
1433
|
function escapeHtml(value) {
|
|
1392
1434
|
return String(value ?? '')
|
|
@@ -3105,14 +3147,17 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
3105
3147
|
res.end(JSON.stringify(hubClientPortPreflightError(portPreflight)));
|
|
3106
3148
|
return;
|
|
3107
3149
|
}
|
|
3108
|
-
const session = await
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3150
|
+
const session = await activateRoleChangeSession(
|
|
3151
|
+
supabase,
|
|
3152
|
+
dashboardState.choice?.session || savedSession
|
|
3153
|
+
);
|
|
3154
|
+
const expectedRoleVersion = Number(dashboardState.roleVersion || 0);
|
|
3155
|
+
const { data, error } = await enqueueDeviceRoleMutation(() => supabase.rpc('set_livedesk_device_role', {
|
|
3156
|
+
p_device_id: deviceId,
|
|
3157
|
+
p_role: 'hub',
|
|
3158
|
+
p_assigned_hub_id: null,
|
|
3159
|
+
p_expected_role_version: expectedRoleVersion > 0 ? expectedRoleVersion : null
|
|
3160
|
+
}));
|
|
3116
3161
|
const result = Array.isArray(data) ? data[0] : data;
|
|
3117
3162
|
if (error || result?.ok === false || !session?.access_token) {
|
|
3118
3163
|
const reason = error?.message || result?.reason || 'role-change-rejected';
|
|
@@ -3415,35 +3460,48 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3415
3460
|
relayEndpoint: options.relayEndpoint
|
|
3416
3461
|
});
|
|
3417
3462
|
},
|
|
3418
|
-
changeRole: async (role, snapshot) => {
|
|
3463
|
+
changeRole: async (role, snapshot, currentSession) => {
|
|
3464
|
+
console.log('[LiveDesk Client] Hub role transition requested. Validating the signed-in session and local Hub port.');
|
|
3419
3465
|
const activeSupabase = await getSupabase();
|
|
3420
3466
|
if (!activeSupabase) {
|
|
3421
|
-
|
|
3467
|
+
console.warn('[LiveDesk Client] Hub role transition rejected: Google sign-in is temporarily unavailable.');
|
|
3468
|
+
return { ok: false, error: 'Google sign-in is temporarily unavailable. Try again in a moment.' };
|
|
3422
3469
|
}
|
|
3423
3470
|
const portPreflight = await preflightHubClientPort();
|
|
3424
3471
|
if (!portPreflight.ok) {
|
|
3472
|
+
console.warn(`[LiveDesk Client] Hub role transition rejected: ${hubClientPortPreflightError(portPreflight).error}`);
|
|
3425
3473
|
return hubClientPortPreflightError(portPreflight);
|
|
3426
3474
|
}
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
}
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
}
|
|
3438
|
-
const
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3475
|
+
let session;
|
|
3476
|
+
try {
|
|
3477
|
+
session = await activateRoleChangeSession(activeSupabase, currentSession);
|
|
3478
|
+
} catch (error) {
|
|
3479
|
+
console.warn(`[LiveDesk Client] Hub role transition rejected: ${formatDiscoveryError(error)}`);
|
|
3480
|
+
return { ok: false, error: formatDiscoveryError(error) };
|
|
3481
|
+
}
|
|
3482
|
+
if (!session?.access_token) {
|
|
3483
|
+
console.warn('[LiveDesk Client] Hub role transition rejected: a fresh Google sign-in is required.');
|
|
3484
|
+
return { ok: false, error: 'Sign in again before switching this computer to Hub.' };
|
|
3485
|
+
}
|
|
3486
|
+
const expectedRoleVersion = Number(snapshot?.roleVersion || 0);
|
|
3487
|
+
const { data, error } = await enqueueDeviceRoleMutation(() => activeSupabase.rpc('set_livedesk_device_role', {
|
|
3488
|
+
p_device_id: options.deviceId,
|
|
3489
|
+
p_role: role,
|
|
3490
|
+
p_assigned_hub_id: null,
|
|
3491
|
+
p_expected_role_version: expectedRoleVersion > 0 ? expectedRoleVersion : null
|
|
3492
|
+
}));
|
|
3493
|
+
const result = Array.isArray(data) ? data[0] : data;
|
|
3494
|
+
if (error || result?.ok === false) {
|
|
3495
|
+
const reason = error?.message || result?.reason || 'role-change-rejected';
|
|
3496
|
+
console.warn(`[LiveDesk Client] Hub role transition rejected: ${reason}`);
|
|
3497
|
+
return { ok: false, error: reason };
|
|
3498
|
+
}
|
|
3442
3499
|
const roleVersion = Number.isInteger(result?.role_version) ? result.role_version : expectedRoleVersion + 1;
|
|
3443
3500
|
writeLocalRoleCache(role, options.deviceId, roleVersion);
|
|
3444
3501
|
roleRestartRequest = { role, requestedAt: new Date().toISOString() };
|
|
3445
3502
|
requestLocalDiscoveryWake('role-change');
|
|
3446
3503
|
try { requestActiveAgentStop(); } catch { /* the lifecycle loop observes the role request */ }
|
|
3504
|
+
console.log(`[LiveDesk Client] Hub role transition accepted at role revision ${roleVersion}. Starting the Hub runtime.`);
|
|
3447
3505
|
return { ok: true, restarting: true, role, roleVersion };
|
|
3448
3506
|
},
|
|
3449
3507
|
videoAcceleration: linuxVideoAccelerationStatus,
|
|
@@ -3853,7 +3911,9 @@ export async function runFreshHubRegistryLookup(resolveFreshTarget, options = {}
|
|
|
3853
3911
|
}
|
|
3854
3912
|
const timeoutMs = normalizeFreshRegistryTimeoutMs(options.timeoutMs);
|
|
3855
3913
|
const controller = new AbortController();
|
|
3914
|
+
const externalSignal = options.signal;
|
|
3856
3915
|
let timeoutHandle = null;
|
|
3916
|
+
let removeExternalAbort = () => undefined;
|
|
3857
3917
|
const lookupOutcome = Promise.resolve()
|
|
3858
3918
|
.then(() => resolveFreshTarget(controller.signal))
|
|
3859
3919
|
.then(
|
|
@@ -3867,6 +3927,17 @@ export async function runFreshHubRegistryLookup(resolveFreshTarget, options = {}
|
|
|
3867
3927
|
);
|
|
3868
3928
|
});
|
|
3869
3929
|
const outcomes = [lookupOutcome, timeoutOutcome];
|
|
3930
|
+
if (externalSignal) {
|
|
3931
|
+
outcomes.push(new Promise(resolveAbort => {
|
|
3932
|
+
const onAbort = () => resolveAbort({ type: 'local-trigger' });
|
|
3933
|
+
if (externalSignal.aborted) {
|
|
3934
|
+
onAbort();
|
|
3935
|
+
return;
|
|
3936
|
+
}
|
|
3937
|
+
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
3938
|
+
removeExternalAbort = () => externalSignal.removeEventListener('abort', onAbort);
|
|
3939
|
+
}));
|
|
3940
|
+
}
|
|
3870
3941
|
if (options.wakePromise && typeof options.wakePromise.then === 'function') {
|
|
3871
3942
|
outcomes.push(Promise.resolve(options.wakePromise).then(
|
|
3872
3943
|
event => ({ type: 'hub-online', event }),
|
|
@@ -3875,7 +3946,12 @@ export async function runFreshHubRegistryLookup(resolveFreshTarget, options = {}
|
|
|
3875
3946
|
}
|
|
3876
3947
|
const outcome = await Promise.race(outcomes).finally(() => {
|
|
3877
3948
|
clearTimeout(timeoutHandle);
|
|
3949
|
+
removeExternalAbort();
|
|
3878
3950
|
});
|
|
3951
|
+
if (outcome.type === 'local-trigger') {
|
|
3952
|
+
controller.abort('local-trigger');
|
|
3953
|
+
return outcome;
|
|
3954
|
+
}
|
|
3879
3955
|
if (outcome.type === 'hub-online') {
|
|
3880
3956
|
controller.abort('hub-online');
|
|
3881
3957
|
return outcome;
|
|
@@ -4050,22 +4126,33 @@ export async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
|
4050
4126
|
};
|
|
4051
4127
|
}
|
|
4052
4128
|
|
|
4053
|
-
async function registerClientDeviceWithSupabase(supabase, options = {}) {
|
|
4054
|
-
if (!options.deviceId || !options.session?.access_token) {
|
|
4055
|
-
return null;
|
|
4056
|
-
}
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4129
|
+
async function registerClientDeviceWithSupabase(supabase, options = {}) {
|
|
4130
|
+
if (!options.deviceId || !options.session?.access_token) {
|
|
4131
|
+
return null;
|
|
4132
|
+
}
|
|
4133
|
+
if (roleRestartRequest?.role) {
|
|
4134
|
+
return { ok: false, skipped: true, reason: 'role-transition-pending' };
|
|
4135
|
+
}
|
|
4136
|
+
try {
|
|
4137
|
+
const { data, error } = await enqueueDeviceRoleMutation(() => {
|
|
4138
|
+
if (roleRestartRequest?.role) {
|
|
4139
|
+
return { data: { ok: false, skipped: true, reason: 'role-transition-pending' }, error: null };
|
|
4140
|
+
}
|
|
4141
|
+
return supabase.rpc('register_livedesk_device', {
|
|
4142
|
+
p_device_id: options.deviceId,
|
|
4143
|
+
p_device_name: options.deviceName || os.hostname(),
|
|
4144
|
+
p_role: 'client',
|
|
4145
|
+
p_assigned_hub_id: options.assignedHubId || null,
|
|
4146
|
+
p_platform: os.platform(),
|
|
4147
|
+
p_os_version: os.release(),
|
|
4148
|
+
p_app_version: readPackageVersion()
|
|
4149
|
+
});
|
|
4150
|
+
});
|
|
4151
|
+
const result = Array.isArray(data) ? data[0] : data;
|
|
4152
|
+
if (result?.skipped === true && result?.reason === 'role-transition-pending') {
|
|
4153
|
+
return result;
|
|
4154
|
+
}
|
|
4155
|
+
if (error || result?.ok === false) {
|
|
4069
4156
|
console.warn(`[LiveDesk Client] Device role registration unavailable: ${error?.message || result?.reason || 'unknown-error'}`);
|
|
4070
4157
|
}
|
|
4071
4158
|
return error ? null : result;
|
|
@@ -4117,9 +4204,14 @@ export async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
4117
4204
|
}),
|
|
4118
4205
|
{
|
|
4119
4206
|
timeoutMs: options.freshTimeoutMs,
|
|
4120
|
-
wakePromise: wakeListener?.promise
|
|
4207
|
+
wakePromise: wakeListener?.promise,
|
|
4208
|
+
signal: discoveryWakeController.signal
|
|
4121
4209
|
}
|
|
4122
4210
|
);
|
|
4211
|
+
if (outcome.type === 'local-trigger') {
|
|
4212
|
+
if (shouldStop()) return null;
|
|
4213
|
+
continue;
|
|
4214
|
+
}
|
|
4123
4215
|
if (outcome.type === 'hub-online') {
|
|
4124
4216
|
wakeListener?.close();
|
|
4125
4217
|
wakeListener = null;
|
|
@@ -4285,6 +4377,18 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
|
|
|
4285
4377
|
} catch (error) {
|
|
4286
4378
|
initialDiscoveryError = error;
|
|
4287
4379
|
}
|
|
4380
|
+
if (roleRestartRequest?.role) {
|
|
4381
|
+
return {
|
|
4382
|
+
...parsed,
|
|
4383
|
+
manager,
|
|
4384
|
+
pair,
|
|
4385
|
+
slot: normalizeSlotNumber(parsed.slot),
|
|
4386
|
+
connectionPage,
|
|
4387
|
+
forwarded,
|
|
4388
|
+
rediscoverOnDisconnect: shouldLogin,
|
|
4389
|
+
rediscoverOnInvalidPair: shouldLogin
|
|
4390
|
+
};
|
|
4391
|
+
}
|
|
4288
4392
|
discoverySource = resolved?.discoverySource || 'supabase';
|
|
4289
4393
|
if (resolved?.discoverySource === 'cache') {
|
|
4290
4394
|
writeSavedSessionToFile(session);
|
|
@@ -4303,7 +4407,7 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
|
|
|
4303
4407
|
});
|
|
4304
4408
|
discoverySource = 'supabase';
|
|
4305
4409
|
}
|
|
4306
|
-
if (
|
|
4410
|
+
if (roleRestartRequest?.role) {
|
|
4307
4411
|
return {
|
|
4308
4412
|
...parsed,
|
|
4309
4413
|
manager,
|
package/package.json
CHANGED
|
@@ -1856,7 +1856,11 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1856
1856
|
return;
|
|
1857
1857
|
}
|
|
1858
1858
|
try {
|
|
1859
|
-
const result = await options.changeRole?.(
|
|
1859
|
+
const result = await options.changeRole?.(
|
|
1860
|
+
'hub',
|
|
1861
|
+
runtime.getSnapshot(),
|
|
1862
|
+
lastChoice?.session || savedSession || null
|
|
1863
|
+
);
|
|
1860
1864
|
respondJson(result?.ok === false ? 409 : 200, result || { ok: false, error: 'role-change-unavailable' });
|
|
1861
1865
|
} catch (error) {
|
|
1862
1866
|
respondJson(409, { ok: false, error: normalizeString(error?.message || error) });
|
|
@@ -1870,7 +1874,11 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1870
1874
|
return;
|
|
1871
1875
|
}
|
|
1872
1876
|
try {
|
|
1873
|
-
const result = await options.changeRole?.(
|
|
1877
|
+
const result = await options.changeRole?.(
|
|
1878
|
+
'hub',
|
|
1879
|
+
runtime.getSnapshot(),
|
|
1880
|
+
lastChoice?.session || savedSession || null
|
|
1881
|
+
);
|
|
1874
1882
|
respondJson(result?.ok === false ? 409 : 200, result || { ok: false, error: 'role-change-unavailable' });
|
|
1875
1883
|
} catch (error) {
|
|
1876
1884
|
respondJson(409, { ok: false, error: normalizeString(error?.message || error) });
|