livedesk 0.1.631 → 0.1.633
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 +469 -116
- package/client/package.json +1 -1
- package/client/src/runtime/hub-wake-listener.js +316 -91
- package/electron/main.mjs +213 -103
- package/hub/package.json +3 -3
- package/hub/src/auth/workspace-access.js +176 -0
- package/hub/src/console-direct.js +469 -11
- package/hub/src/console-direct.test.mjs +446 -5
- package/hub/src/http/hub-ui-session.js +155 -32
- package/hub/src/server.js +1141 -328
- package/package.json +2 -2
- package/web/dist/app.html +7 -7
- package/web/dist/app.webmanifest +1 -1
- package/web/dist/assets/{AgentSettingsTab-VwiC7JUN.js → AgentSettingsTab-DyPu-H5o.js} +1 -1
- package/web/dist/assets/{AgentsPage-DhvGz3nK.js → AgentsPage-DY3Cg-DH.js} +1 -1
- package/web/dist/assets/App-DFLBNvIi.css +1 -0
- package/web/dist/assets/App-DwVqfExL.js +181 -0
- package/web/dist/assets/{CaptureGallery-aelE6Zdk.js → CaptureGallery-D9ArARgM.js} +1 -1
- package/web/dist/assets/{DesktopUpdatePanel-_K61p838.js → DesktopUpdatePanel-B3aUfd1G.js} +1 -1
- package/web/dist/assets/LiveDeskApp-DPIrXfaj.js +1 -0
- package/web/dist/assets/{SettingsPage-pt2aO7OB.js → SettingsPage-BcPHDfSH.js} +1 -1
- package/web/dist/assets/SettingsTabs-DFoU16NX.js +1 -0
- package/web/dist/assets/{ShareFilesPage-DHSxFlMx.js → ShareFilesPage-Cdu46CVM.js} +1 -1
- package/web/dist/assets/{SupportPage-D4TV6Vpp.js → SupportPage-C8rh3fLm.js} +2 -2
- package/web/dist/assets/app-Colp5UPd.js +1 -0
- package/web/dist/assets/icons-CwWL39Up.js +1 -0
- package/web/dist/assets/{main-BzVdJN3b.css → main-C1HTJiPX.css} +1 -1
- package/web/dist/assets/main-hfSMYb-x.js +2 -0
- package/web/dist/assets/{react-CzpJ4iNd.js → react-ITwfzPyo.js} +1 -1
- package/web/dist/assets/styles-Dft03_BF.js +16 -0
- package/web/dist/assets/styles-PqoeFg2_.css +1 -0
- package/web/dist/index.html +6 -6
- package/web/dist/livedesk-build-evidence.json +58 -58
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/App-B6lW0dqi.js +0 -171
- package/web/dist/assets/App-CD73TEql.css +0 -1
- package/web/dist/assets/LiveDeskApp-DtvEkJkj.js +0 -1
- package/web/dist/assets/SettingsTabs-Crjk_9s3.js +0 -1
- package/web/dist/assets/app-Dz24Poiu.js +0 -1
- package/web/dist/assets/icons-C9gCrfh8.js +0 -1
- package/web/dist/assets/main-DvaMzUeB.js +0 -2
- package/web/dist/assets/styles-BNaoQWzp.css +0 -1
- package/web/dist/assets/styles-BQJexgrQ.js +0 -12
|
@@ -53,8 +53,9 @@ const EXIT_INVALID_PAIR_TOKEN = 23;
|
|
|
53
53
|
const EXIT_CLIENT_UPDATE = 42;
|
|
54
54
|
const SESSION_REFRESH_SKEW_SECONDS = 60;
|
|
55
55
|
const SUPABASE_REQUEST_TIMEOUT_MS = 8_000;
|
|
56
|
-
const HUB_TARGET_CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
57
|
-
const HUB_TARGET_CACHE_FUTURE_SKEW_MS = 5 * 60 * 1000;
|
|
56
|
+
const HUB_TARGET_CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
57
|
+
const HUB_TARGET_CACHE_FUTURE_SKEW_MS = 5 * 60 * 1000;
|
|
58
|
+
const WORKSPACE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
58
59
|
const WINDOWS_STARTUP_SCRIPT_NAME = 'VuvoDesk Desktop.vbs';
|
|
59
60
|
const WINDOWS_STARTUP_LEGACY_DESKTOP_SCRIPT_NAME = 'LiveDesk Desktop.vbs';
|
|
60
61
|
const WINDOWS_STARTUP_LEGACY_SCRIPT_NAME = 'LiveDesk Client.vbs';
|
|
@@ -1006,10 +1007,23 @@ function writeClientUpdateRuntimeStage(stage, details = {}) {
|
|
|
1006
1007
|
}
|
|
1007
1008
|
}
|
|
1008
1009
|
|
|
1009
|
-
function
|
|
1010
|
-
const
|
|
1011
|
-
return
|
|
1012
|
-
}
|
|
1010
|
+
function normalizeWorkspaceId(value) {
|
|
1011
|
+
const workspaceId = String(value || '').trim();
|
|
1012
|
+
return WORKSPACE_ID_PATTERN.test(workspaceId) ? workspaceId : '';
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
export function hubCacheOwnerForSession(session, workspaceId = '') {
|
|
1016
|
+
const userId = String(session?.user?.id || '').trim();
|
|
1017
|
+
if (!userId) return '';
|
|
1018
|
+
const normalizedWorkspaceId = normalizeWorkspaceId(workspaceId);
|
|
1019
|
+
// Personal workspace ids intentionally equal auth.uid. Preserve the old
|
|
1020
|
+
// cache owner key so every existing Personal installation keeps its
|
|
1021
|
+
// verified Hub cache after this upgrade. Team caches carry the workspace
|
|
1022
|
+
// id so switching between Personal and Team can never reuse a pair token.
|
|
1023
|
+
return normalizedWorkspaceId && normalizedWorkspaceId !== userId
|
|
1024
|
+
? `user:${userId}:workspace:${normalizedWorkspaceId}`
|
|
1025
|
+
: `user:${userId}`;
|
|
1026
|
+
}
|
|
1013
1027
|
|
|
1014
1028
|
function hubCacheOwnerForPin(pin) {
|
|
1015
1029
|
const normalizedPin = normalizePairingPin(pin);
|
|
@@ -1049,7 +1063,7 @@ export function normalizeCachedHubTarget(value, expectedOwnerKey, now = Date.now
|
|
|
1049
1063
|
};
|
|
1050
1064
|
}
|
|
1051
1065
|
|
|
1052
|
-
export function readCachedHubTarget(ownerKey) {
|
|
1066
|
+
export function readCachedHubTarget(ownerKey) {
|
|
1053
1067
|
const paths = [...new Set([preferredClientAuthPath(), CLIENT_AUTH_PATH, UNIFIED_CLIENT_AUTH_PATH])];
|
|
1054
1068
|
for (const path of paths) {
|
|
1055
1069
|
try {
|
|
@@ -1062,10 +1076,43 @@ export function readCachedHubTarget(ownerKey) {
|
|
|
1062
1076
|
// Try the next compatible state location.
|
|
1063
1077
|
}
|
|
1064
1078
|
}
|
|
1065
|
-
return null;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
export function
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
export function readCachedHubWorkspaceBinding(session, now = Date.now()) {
|
|
1083
|
+
const userId = normalizeWorkspaceId(session?.user?.id);
|
|
1084
|
+
if (!userId) return null;
|
|
1085
|
+
const personalOwnerKey = `user:${userId}`;
|
|
1086
|
+
const teamOwnerPrefix = `${personalOwnerKey}:workspace:`;
|
|
1087
|
+
const paths = [...new Set([preferredClientAuthPath(), CLIENT_AUTH_PATH, UNIFIED_CLIENT_AUTH_PATH])];
|
|
1088
|
+
for (const path of paths) {
|
|
1089
|
+
try {
|
|
1090
|
+
const state = JSON.parse(readFileSync(path, 'utf8'));
|
|
1091
|
+
const raw = state?.[CLIENT_HUB_TARGET_CACHE_STORAGE_KEY];
|
|
1092
|
+
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
1093
|
+
const ownerKey = String(parsed?.ownerKey || '').trim();
|
|
1094
|
+
let workspaceId = '';
|
|
1095
|
+
if (ownerKey === personalOwnerKey) {
|
|
1096
|
+
workspaceId = userId;
|
|
1097
|
+
} else if (ownerKey.startsWith(teamOwnerPrefix)) {
|
|
1098
|
+
workspaceId = normalizeWorkspaceId(ownerKey.slice(teamOwnerPrefix.length));
|
|
1099
|
+
}
|
|
1100
|
+
if (!workspaceId || !normalizeCachedHubTarget(parsed, ownerKey, now)) continue;
|
|
1101
|
+
return { ownerKey, workspaceId };
|
|
1102
|
+
} catch {
|
|
1103
|
+
// Try the next compatible state location.
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
export function hasClientWorkspaceCacheOwnerChanged(cachedBinding, session, freshWorkspaceId) {
|
|
1110
|
+
const previousOwnerKey = String(cachedBinding?.ownerKey || '').trim();
|
|
1111
|
+
if (!previousOwnerKey) return false;
|
|
1112
|
+
return hubCacheOwnerForSession(session, freshWorkspaceId) !== previousOwnerKey;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
export function writeCachedHubTarget(ownerKey, target) {
|
|
1069
1116
|
const normalizedOwner = String(ownerKey || '').trim();
|
|
1070
1117
|
const pair = String(target?.pair || '').trim();
|
|
1071
1118
|
const endpointCandidates = normalizeEndpointCandidates({
|
|
@@ -4048,33 +4095,142 @@ function waitForDiscoveryTrigger(delayMs, wakePromise = null, shouldStop = () =>
|
|
|
4048
4095
|
});
|
|
4049
4096
|
}
|
|
4050
4097
|
|
|
4051
|
-
function normalizeEndpointCandidates(target) {
|
|
4098
|
+
function normalizeEndpointCandidates(target) {
|
|
4052
4099
|
const rawCandidates = [
|
|
4053
4100
|
target?.endpoint,
|
|
4054
4101
|
...(Array.isArray(target?.endpoint_candidates) ? target.endpoint_candidates : [])
|
|
4055
4102
|
];
|
|
4056
|
-
return [...new Set(rawCandidates.map(value => String(value || '').trim()).filter(Boolean))];
|
|
4057
|
-
}
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4103
|
+
return [...new Set(rawCandidates.map(value => String(value || '').trim()).filter(Boolean))];
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
function isLegacyWorkspaceSchemaError(error) {
|
|
4107
|
+
const code = String(error?.code || '').trim().toUpperCase();
|
|
4108
|
+
const message = String(error?.message || error || '');
|
|
4109
|
+
return code === '42703'
|
|
4110
|
+
|| code === 'PGRST202'
|
|
4111
|
+
|| /workspace_id.*does not exist|current_livedesk_owner_workspace.*not found/i.test(message);
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
export async function resolveClientWorkspaceFromSupabase(supabase, options = {}) {
|
|
4115
|
+
const deviceId = String(options.deviceId || '').trim();
|
|
4116
|
+
const session = options.session?.access_token
|
|
4117
|
+
? options.session
|
|
4118
|
+
: await refreshSessionIfNeeded(supabase);
|
|
4119
|
+
const userId = normalizeWorkspaceId(session?.user?.id);
|
|
4120
|
+
if (!userId) {
|
|
4121
|
+
throw createHubDiscoveryError(
|
|
4122
|
+
'workspace-session-required',
|
|
4123
|
+
'A signed-in VuvoDesk account is required to resolve this Client workspace.'
|
|
4124
|
+
);
|
|
4125
|
+
}
|
|
4126
|
+
if (!deviceId) {
|
|
4127
|
+
throw createHubDiscoveryError(
|
|
4128
|
+
'workspace-device-id-required',
|
|
4129
|
+
'The VuvoDesk Client device identity is required to resolve its workspace.'
|
|
4130
|
+
);
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4133
|
+
let query = supabase
|
|
4134
|
+
.from('livedesk_devices')
|
|
4135
|
+
.select('workspace_id, device_id, enabled')
|
|
4136
|
+
.eq('account_id', userId)
|
|
4137
|
+
.eq('device_id', deviceId)
|
|
4138
|
+
.limit(2);
|
|
4139
|
+
if (options.signal && typeof query.abortSignal === 'function') {
|
|
4140
|
+
query = query.abortSignal(options.signal);
|
|
4141
|
+
}
|
|
4142
|
+
const { data, error } = await query;
|
|
4143
|
+
if (error) {
|
|
4144
|
+
// A pre-Team Supabase schema has only one possible Personal namespace,
|
|
4145
|
+
// whose id is auth.uid. This fallback preserves deployed Personal
|
|
4146
|
+
// Clients while never guessing after the workspace schema exists.
|
|
4147
|
+
if (isLegacyWorkspaceSchemaError(error)) return userId;
|
|
4148
|
+
throw error;
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
const rows = Array.isArray(data) ? data : [];
|
|
4152
|
+
const workspaceIds = [...new Set(rows
|
|
4153
|
+
.map(row => normalizeWorkspaceId(row?.workspace_id))
|
|
4154
|
+
.filter(Boolean))];
|
|
4155
|
+
if (workspaceIds.length > 1) {
|
|
4156
|
+
throw createHubDiscoveryError(
|
|
4157
|
+
'workspace-binding-conflict',
|
|
4158
|
+
'This VuvoDesk Client device is bound to more than one workspace.'
|
|
4159
|
+
);
|
|
4160
|
+
}
|
|
4161
|
+
if (rows.length > 0 && workspaceIds.length !== 1) {
|
|
4162
|
+
throw createHubDiscoveryError(
|
|
4163
|
+
'workspace-binding-invalid',
|
|
4164
|
+
'This VuvoDesk Client device has an invalid workspace binding.'
|
|
4165
|
+
);
|
|
4166
|
+
}
|
|
4167
|
+
if (rows.some(row => row?.enabled === false)) {
|
|
4168
|
+
throw createHubDiscoveryError(
|
|
4169
|
+
'workspace-device-disabled',
|
|
4170
|
+
'This VuvoDesk Client device is disabled.'
|
|
4171
|
+
);
|
|
4172
|
+
}
|
|
4173
|
+
if (workspaceIds.length === 1) {
|
|
4174
|
+
return workspaceIds[0];
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
const { data: currentWorkspace, error: currentWorkspaceError } = await supabase.rpc(
|
|
4178
|
+
'current_livedesk_owner_workspace'
|
|
4179
|
+
);
|
|
4180
|
+
if (currentWorkspaceError) {
|
|
4181
|
+
if (isLegacyWorkspaceSchemaError(currentWorkspaceError)) return userId;
|
|
4182
|
+
throw currentWorkspaceError;
|
|
4183
|
+
}
|
|
4184
|
+
const currentValue = Array.isArray(currentWorkspace)
|
|
4185
|
+
? currentWorkspace[0]?.current_livedesk_owner_workspace ?? currentWorkspace[0]
|
|
4186
|
+
: currentWorkspace?.current_livedesk_owner_workspace ?? currentWorkspace;
|
|
4187
|
+
const workspaceId = normalizeWorkspaceId(currentValue);
|
|
4188
|
+
if (!workspaceId) {
|
|
4189
|
+
throw createHubDiscoveryError(
|
|
4190
|
+
'workspace-current-missing',
|
|
4191
|
+
'No current VuvoDesk workspace is available for this new Client installation.'
|
|
4192
|
+
);
|
|
4193
|
+
}
|
|
4194
|
+
return workspaceId;
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
export async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
4064
4198
|
if (options.signal?.aborted) {
|
|
4065
4199
|
throw createHubDiscoveryError('hub-registry-aborted', 'VuvoDesk Hub registry lookup was cancelled.');
|
|
4066
4200
|
}
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4201
|
+
const session = await refreshSessionIfNeeded(supabase);
|
|
4202
|
+
if (options.signal?.aborted) {
|
|
4203
|
+
throw createHubDiscoveryError('hub-registry-aborted', 'VuvoDesk Hub registry lookup was cancelled.');
|
|
4204
|
+
}
|
|
4205
|
+
const workspaceId = normalizeWorkspaceId(options.workspaceId)
|
|
4206
|
+
|| await resolveClientWorkspaceFromSupabase(supabase, {
|
|
4207
|
+
deviceId: options.deviceId,
|
|
4208
|
+
session,
|
|
4209
|
+
signal: options.signal
|
|
4210
|
+
});
|
|
4211
|
+
let query = supabase.rpc('get_livedesk_workspace_remote_host_target', {
|
|
4212
|
+
p_workspace_id: workspaceId
|
|
4213
|
+
});
|
|
4214
|
+
if (options.signal && typeof query.abortSignal === 'function') {
|
|
4215
|
+
query = query.abortSignal(options.signal);
|
|
4216
|
+
}
|
|
4217
|
+
const { data: responseData, error } = await query;
|
|
4218
|
+
if (error) {
|
|
4219
|
+
throw error;
|
|
4220
|
+
}
|
|
4221
|
+
const data = Array.isArray(responseData) ? responseData[0] : responseData;
|
|
4222
|
+
if (data?.ok === false) {
|
|
4223
|
+
throw createHubDiscoveryError(
|
|
4224
|
+
String(data.reason || 'workspace-host-target-access-denied'),
|
|
4225
|
+
'The VuvoDesk Client could not read this workspace Hub target.'
|
|
4226
|
+
);
|
|
4227
|
+
}
|
|
4228
|
+
if (data?.workspace_id && normalizeWorkspaceId(data.workspace_id) !== workspaceId) {
|
|
4229
|
+
throw createHubDiscoveryError(
|
|
4230
|
+
'workspace-target-mismatch',
|
|
4231
|
+
'The VuvoDesk Hub registry returned a different workspace.'
|
|
4232
|
+
);
|
|
4233
|
+
}
|
|
4078
4234
|
if (!data?.active) {
|
|
4079
4235
|
throw createHubDiscoveryError(
|
|
4080
4236
|
'hub-registry-missing',
|
|
@@ -4110,8 +4266,9 @@ export async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
|
4110
4266
|
`No reachable VuvoDesk Hub endpoint yet. Checked ${endpointCandidates.join(', ')}.`
|
|
4111
4267
|
);
|
|
4112
4268
|
}
|
|
4113
|
-
return {
|
|
4114
|
-
|
|
4269
|
+
return {
|
|
4270
|
+
workspaceId,
|
|
4271
|
+
manager: target.manager,
|
|
4115
4272
|
pair: data.pair_token,
|
|
4116
4273
|
hubDeviceId: String(data.node_id || '').trim(),
|
|
4117
4274
|
endpointCandidates,
|
|
@@ -4119,23 +4276,61 @@ export async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
|
4119
4276
|
connectionTransport: target.connectionTransport,
|
|
4120
4277
|
directProbeSkipped: target.directProbeSkipped === true,
|
|
4121
4278
|
directProbeSkipReason: String(target.directProbeSkipReason || '')
|
|
4122
|
-
};
|
|
4123
|
-
}
|
|
4124
|
-
|
|
4125
|
-
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
function readClientInstallationId(deviceId) {
|
|
4283
|
+
const expectedDeviceId = String(deviceId || '').trim();
|
|
4284
|
+
if (!expectedDeviceId) return '';
|
|
4285
|
+
for (const path of [
|
|
4286
|
+
join(UNIFIED_CLIENT_STATE_DIR, 'device.json'),
|
|
4287
|
+
join(CLIENT_STATE_DIR, 'device.json')
|
|
4288
|
+
]) {
|
|
4289
|
+
try {
|
|
4290
|
+
const identity = JSON.parse(readFileSync(path, 'utf8'));
|
|
4291
|
+
if (String(identity?.deviceId || '').trim() !== expectedDeviceId) continue;
|
|
4292
|
+
const installationId = String(identity?.installationId || '').trim();
|
|
4293
|
+
if (installationId) return installationId.slice(0, 128);
|
|
4294
|
+
} catch {
|
|
4295
|
+
// Try the compatible legacy identity path next.
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
return '';
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
async function registerClientDeviceWithSupabase(supabase, options = {}) {
|
|
4126
4302
|
if (!options.deviceId || !options.session?.access_token) {
|
|
4127
4303
|
return null;
|
|
4128
|
-
}
|
|
4129
|
-
try {
|
|
4130
|
-
const
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
}
|
|
4304
|
+
}
|
|
4305
|
+
try {
|
|
4306
|
+
const workspaceId = normalizeWorkspaceId(options.workspaceId);
|
|
4307
|
+
const installationId = readClientInstallationId(options.deviceId);
|
|
4308
|
+
const exactWorkspaceRegistration = Boolean(workspaceId && installationId);
|
|
4309
|
+
if (workspaceId
|
|
4310
|
+
&& workspaceId !== normalizeWorkspaceId(options.session?.user?.id)
|
|
4311
|
+
&& !installationId) {
|
|
4312
|
+
console.warn('[VuvoDesk Client] Team device registration was skipped because the exact installation identity is unavailable.');
|
|
4313
|
+
return null;
|
|
4314
|
+
}
|
|
4315
|
+
const { data, error } = exactWorkspaceRegistration
|
|
4316
|
+
? await supabase.rpc('resolve_or_register_livedesk_workspace_device', {
|
|
4317
|
+
p_workspace_id: workspaceId,
|
|
4318
|
+
p_device_id: options.deviceId,
|
|
4319
|
+
p_device_name: options.deviceName || os.hostname(),
|
|
4320
|
+
p_installation_id: installationId,
|
|
4321
|
+
p_platform: os.platform(),
|
|
4322
|
+
p_os_version: os.release(),
|
|
4323
|
+
p_app_version: readPackageVersion()
|
|
4324
|
+
})
|
|
4325
|
+
: await supabase.rpc('register_livedesk_device', {
|
|
4326
|
+
p_device_id: options.deviceId,
|
|
4327
|
+
p_device_name: options.deviceName || os.hostname(),
|
|
4328
|
+
p_role: 'client',
|
|
4329
|
+
p_assigned_hub_id: options.assignedHubId || null,
|
|
4330
|
+
p_platform: os.platform(),
|
|
4331
|
+
p_os_version: os.release(),
|
|
4332
|
+
p_app_version: readPackageVersion()
|
|
4333
|
+
});
|
|
4139
4334
|
const result = Array.isArray(data) ? data[0] : data;
|
|
4140
4335
|
if (error || result?.ok === false) {
|
|
4141
4336
|
console.warn(`[VuvoDesk Client] Device role registration unavailable: ${error?.message || result?.reason || 'unknown-error'}`);
|
|
@@ -4147,27 +4342,148 @@ async function registerClientDeviceWithSupabase(supabase, options = {}) {
|
|
|
4147
4342
|
}
|
|
4148
4343
|
}
|
|
4149
4344
|
|
|
4150
|
-
export async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
4345
|
+
export async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
4151
4346
|
const configuredIntervalMs = Number(options.intervalMs || 0);
|
|
4152
4347
|
const shouldStop = typeof options.shouldStop === 'function' ? options.shouldStop : () => false;
|
|
4153
4348
|
const createWakeListener = typeof options.createWakeListener === 'function'
|
|
4154
4349
|
? options.createWakeListener
|
|
4155
4350
|
: createHubWakeListener;
|
|
4156
4351
|
let attempts = 0;
|
|
4157
|
-
let lastMessage = '';
|
|
4158
|
-
let wakeListener = null;
|
|
4159
|
-
let initialError = options.initialError || null;
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4352
|
+
let lastMessage = '';
|
|
4353
|
+
let wakeListener = null;
|
|
4354
|
+
let initialError = options.initialError || null;
|
|
4355
|
+
let workspaceId = normalizeWorkspaceId(options.workspaceId);
|
|
4356
|
+
const memberUserId = String(options.memberUserId || '').trim();
|
|
4357
|
+
let workspaceOwnerGeneration = workspaceId ? 1 : 0;
|
|
4358
|
+
const resolveWorkspace = typeof options.resolveWorkspace === 'function'
|
|
4359
|
+
? options.resolveWorkspace
|
|
4360
|
+
: refreshOptions => resolveClientWorkspaceFromSupabase(supabase, refreshOptions);
|
|
4361
|
+
const requestedWorkspaceRefreshIntervalMs = Number(options.workspaceRefreshIntervalMs || 15_000);
|
|
4362
|
+
const workspaceRefreshIntervalMs = Number.isFinite(requestedWorkspaceRefreshIntervalMs)
|
|
4363
|
+
? Math.max(25, requestedWorkspaceRefreshIntervalMs)
|
|
4364
|
+
: 15_000;
|
|
4365
|
+
let workspaceRefreshPromise = null;
|
|
4366
|
+
let lastWorkspaceRefreshAt = 0;
|
|
4367
|
+
|
|
4368
|
+
const refreshWorkspaceBinding = async ({ force = false } = {}) => {
|
|
4369
|
+
if (!String(options.deviceId || '').trim()) return false;
|
|
4370
|
+
const now = Date.now();
|
|
4371
|
+
// Even a forced registry-missing refresh is rate bounded. This keeps a
|
|
4372
|
+
// deleted/expired target from turning into an unbounded device lookup.
|
|
4373
|
+
if (lastWorkspaceRefreshAt > 0
|
|
4374
|
+
&& now - lastWorkspaceRefreshAt < workspaceRefreshIntervalMs) {
|
|
4375
|
+
return false;
|
|
4376
|
+
}
|
|
4377
|
+
if (!force && attempts === 0) return false;
|
|
4378
|
+
if (workspaceRefreshPromise) return workspaceRefreshPromise;
|
|
4379
|
+
|
|
4380
|
+
lastWorkspaceRefreshAt = now;
|
|
4381
|
+
workspaceRefreshPromise = (async () => {
|
|
4382
|
+
try {
|
|
4383
|
+
const nextWorkspaceId = normalizeWorkspaceId(await resolveWorkspace({
|
|
4384
|
+
deviceId: options.deviceId
|
|
4385
|
+
}));
|
|
4386
|
+
if (!nextWorkspaceId || nextWorkspaceId === workspaceId) return false;
|
|
4387
|
+
|
|
4388
|
+
const previousWorkspaceId = workspaceId;
|
|
4389
|
+
workspaceOwnerGeneration += 1;
|
|
4390
|
+
wakeListener?.close();
|
|
4391
|
+
wakeListener = null;
|
|
4392
|
+
workspaceId = nextWorkspaceId;
|
|
4393
|
+
initialError = null;
|
|
4394
|
+
lastMessage = '';
|
|
4395
|
+
console.log(`[VuvoDesk Client] Workspace binding moved from ${previousWorkspaceId || 'unbound'} to ${workspaceId}. Rebinding Hub discovery without restarting.`);
|
|
4396
|
+
return true;
|
|
4397
|
+
} catch (error) {
|
|
4398
|
+
if (force && attempts === 0) {
|
|
4399
|
+
console.warn(`[VuvoDesk Client] Workspace move check unavailable; continuing the exact current workspace: ${error?.message || error}`);
|
|
4400
|
+
}
|
|
4401
|
+
return false;
|
|
4402
|
+
} finally {
|
|
4403
|
+
workspaceRefreshPromise = null;
|
|
4404
|
+
}
|
|
4405
|
+
})();
|
|
4406
|
+
return workspaceRefreshPromise;
|
|
4407
|
+
};
|
|
4408
|
+
|
|
4409
|
+
const shouldRefreshWorkspaceAfterRegistryError = error => {
|
|
4410
|
+
const code = String(error?.code || '').trim().toLowerCase();
|
|
4411
|
+
return code === 'hub-registry-missing'
|
|
4412
|
+
|| code === 'workspace-target-mismatch';
|
|
4413
|
+
};
|
|
4414
|
+
console.log('Waiting for a VuvoDesk Hub. VuvoDesk is listening for Hub-online events with adaptive registry retries as a fallback.');
|
|
4415
|
+
try {
|
|
4416
|
+
while (true) {
|
|
4417
|
+
if (shouldStop()) {
|
|
4418
|
+
return null;
|
|
4419
|
+
}
|
|
4420
|
+
if (!workspaceId) {
|
|
4421
|
+
try {
|
|
4422
|
+
const resolvedWorkspaceId = normalizeWorkspaceId(await resolveWorkspace({
|
|
4423
|
+
deviceId: options.deviceId
|
|
4424
|
+
}));
|
|
4425
|
+
if (!resolvedWorkspaceId) {
|
|
4426
|
+
throw createHubDiscoveryError(
|
|
4427
|
+
'workspace-current-missing',
|
|
4428
|
+
'No current VuvoDesk workspace is available for this Client.'
|
|
4429
|
+
);
|
|
4430
|
+
}
|
|
4431
|
+
workspaceId = resolvedWorkspaceId;
|
|
4432
|
+
workspaceOwnerGeneration += 1;
|
|
4433
|
+
} catch (error) {
|
|
4434
|
+
attempts += 1;
|
|
4435
|
+
const message = formatDiscoveryError(error);
|
|
4436
|
+
if (message !== lastMessage || attempts === 1 || attempts % 6 === 0) {
|
|
4437
|
+
const suffix = attempts === 1 ? '' : ` attempt ${attempts}`;
|
|
4438
|
+
console.log(`Still waiting for VuvoDesk workspace${suffix}: ${message}`);
|
|
4439
|
+
lastMessage = message;
|
|
4440
|
+
}
|
|
4441
|
+
await waitForDiscoveryTrigger(
|
|
4442
|
+
getDiscoveryRetryDelay(attempts, configuredIntervalMs),
|
|
4443
|
+
null,
|
|
4444
|
+
shouldStop
|
|
4445
|
+
);
|
|
4446
|
+
continue;
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
if (await refreshWorkspaceBinding()) {
|
|
4450
|
+
continue;
|
|
4451
|
+
}
|
|
4452
|
+
if (!wakeListener) {
|
|
4453
|
+
try {
|
|
4454
|
+
const listenerWorkspaceId = workspaceId;
|
|
4455
|
+
const listenerOwnerGeneration = workspaceOwnerGeneration;
|
|
4456
|
+
wakeListener = await createWakeListener({
|
|
4457
|
+
workspaceId: listenerWorkspaceId,
|
|
4458
|
+
memberUserId,
|
|
4459
|
+
ownerGeneration: listenerOwnerGeneration,
|
|
4460
|
+
getAccessContext: async expected => {
|
|
4461
|
+
if (workspaceId !== listenerWorkspaceId
|
|
4462
|
+
|| workspaceOwnerGeneration !== listenerOwnerGeneration
|
|
4463
|
+
|| expected?.workspaceId !== listenerWorkspaceId
|
|
4464
|
+
|| String(expected?.memberUserId || '') !== memberUserId
|
|
4465
|
+
|| String(expected?.ownerGeneration ?? '') !== String(listenerOwnerGeneration)) {
|
|
4466
|
+
return null;
|
|
4467
|
+
}
|
|
4468
|
+
const session = await refreshSessionIfNeeded(supabase);
|
|
4469
|
+
const refreshedMemberUserId = String(session?.user?.id || '').trim();
|
|
4470
|
+
const accessToken = String(session?.access_token || '').trim();
|
|
4471
|
+
if (!accessToken
|
|
4472
|
+
|| !refreshedMemberUserId
|
|
4473
|
+
|| refreshedMemberUserId !== memberUserId
|
|
4474
|
+
|| workspaceId !== listenerWorkspaceId
|
|
4475
|
+
|| workspaceOwnerGeneration !== listenerOwnerGeneration) {
|
|
4476
|
+
return null;
|
|
4477
|
+
}
|
|
4478
|
+
return {
|
|
4479
|
+
accessToken,
|
|
4480
|
+
workspaceId: listenerWorkspaceId,
|
|
4481
|
+
memberUserId,
|
|
4482
|
+
ownerGeneration: listenerOwnerGeneration,
|
|
4483
|
+
connectionGeneration: expected.connectionGeneration
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
4486
|
+
});
|
|
4171
4487
|
} catch (error) {
|
|
4172
4488
|
if (attempts === 0) {
|
|
4173
4489
|
console.warn(`[VuvoDesk Client] Hub-online event channel unavailable; adaptive registry retries remain active: ${error?.message || error}`);
|
|
@@ -4182,11 +4498,12 @@ export async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
4182
4498
|
throw error;
|
|
4183
4499
|
}
|
|
4184
4500
|
const outcome = await runFreshHubRegistryLookup(
|
|
4185
|
-
signal => resolveManagerFromSupabase(supabase, {
|
|
4186
|
-
allowRelayFallback: options.allowRelayFallback === true,
|
|
4187
|
-
probeEndpoint: options.probeEndpoint,
|
|
4188
|
-
|
|
4189
|
-
|
|
4501
|
+
signal => resolveManagerFromSupabase(supabase, {
|
|
4502
|
+
allowRelayFallback: options.allowRelayFallback === true,
|
|
4503
|
+
probeEndpoint: options.probeEndpoint,
|
|
4504
|
+
workspaceId,
|
|
4505
|
+
signal
|
|
4506
|
+
}),
|
|
4190
4507
|
{
|
|
4191
4508
|
timeoutMs: options.freshTimeoutMs,
|
|
4192
4509
|
wakePromise: wakeListener?.promise
|
|
@@ -4199,8 +4516,12 @@ export async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
4199
4516
|
continue;
|
|
4200
4517
|
}
|
|
4201
4518
|
return outcome.target;
|
|
4202
|
-
} catch (err) {
|
|
4203
|
-
|
|
4519
|
+
} catch (err) {
|
|
4520
|
+
if (shouldRefreshWorkspaceAfterRegistryError(err)
|
|
4521
|
+
&& await refreshWorkspaceBinding({ force: true })) {
|
|
4522
|
+
continue;
|
|
4523
|
+
}
|
|
4524
|
+
const message = formatDiscoveryError(err);
|
|
4204
4525
|
if (message !== lastMessage || attempts === 1 || attempts % 6 === 0) {
|
|
4205
4526
|
const suffix = attempts === 1 ? '' : ` attempt ${attempts}`;
|
|
4206
4527
|
console.log(`Still waiting for VuvoDesk Hub${suffix}: ${message}`);
|
|
@@ -4327,54 +4648,83 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
|
|
|
4327
4648
|
console.log(connectionTransport === 'relay-fallback'
|
|
4328
4649
|
? 'VuvoDesk PIN accepted. Direct TCP is unavailable; starting encrypted relay fallback.'
|
|
4329
4650
|
: 'Connected by VuvoDesk PIN.');
|
|
4330
|
-
} else {
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
let
|
|
4334
|
-
let
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
//
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
session =
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4651
|
+
} else {
|
|
4652
|
+
let session = choice.session;
|
|
4653
|
+
const cachedWorkspaceBinding = readCachedHubWorkspaceBinding(session);
|
|
4654
|
+
let workspaceId = cachedWorkspaceBinding?.workspaceId || '';
|
|
4655
|
+
let cacheOwnerKey = cachedWorkspaceBinding?.ownerKey
|
|
4656
|
+
|| hubCacheOwnerForSession(session, session?.user?.id);
|
|
4657
|
+
let initialDiscoveryError = null;
|
|
4658
|
+
let resolved = null;
|
|
4659
|
+
try {
|
|
4660
|
+
resolved = await resolveManagerWithCachedRecovery(cacheOwnerKey, {
|
|
4661
|
+
allowRelayFallback,
|
|
4662
|
+
relayEndpoint: parsed.relay,
|
|
4663
|
+
resolveFreshTarget: async signal => {
|
|
4664
|
+
// The signed registry is authoritative for Hub replacement
|
|
4665
|
+
// and pair-token rotation. Cache direct/relay recovery is
|
|
4666
|
+
// considered only after this one current lookup reports
|
|
4667
|
+
// an explicitly recoverable availability failure.
|
|
4668
|
+
try {
|
|
4669
|
+
session = await activateSupabaseSession(supabase, choice.session);
|
|
4670
|
+
} catch (error) {
|
|
4671
|
+
if (!isRefreshTokenAlreadyUsedError(error)) throw error;
|
|
4672
|
+
session = readSavedSessionFromFile() || choice.session;
|
|
4673
|
+
console.warn('[VuvoDesk Client] Another auth owner rotated the refresh token. Keeping the runtime alive while the saved session catches up.');
|
|
4674
|
+
}
|
|
4675
|
+
const freshWorkspaceId = await resolveClientWorkspaceFromSupabase(supabase, {
|
|
4676
|
+
deviceId: parsed.deviceId,
|
|
4677
|
+
session,
|
|
4678
|
+
signal
|
|
4679
|
+
});
|
|
4680
|
+
const freshCacheOwnerKey = hubCacheOwnerForSession(session, freshWorkspaceId);
|
|
4681
|
+
const cacheWorkspaceChanged = hasClientWorkspaceCacheOwnerChanged(
|
|
4682
|
+
cachedWorkspaceBinding,
|
|
4683
|
+
session,
|
|
4684
|
+
freshWorkspaceId
|
|
4685
|
+
);
|
|
4686
|
+
workspaceId = freshWorkspaceId;
|
|
4687
|
+
cacheOwnerKey = freshCacheOwnerKey;
|
|
4688
|
+
try {
|
|
4689
|
+
return await resolveManagerFromSupabase(supabase, {
|
|
4690
|
+
allowRelayFallback,
|
|
4691
|
+
workspaceId,
|
|
4692
|
+
signal
|
|
4693
|
+
});
|
|
4694
|
+
} catch (error) {
|
|
4695
|
+
if (!cacheWorkspaceChanged) throw error;
|
|
4696
|
+
throw createHubDiscoveryError(
|
|
4697
|
+
'workspace-cache-owner-changed',
|
|
4698
|
+
'The Client workspace changed, so the previous workspace Hub cache was rejected.',
|
|
4699
|
+
error
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
});
|
|
4357
4704
|
} catch (error) {
|
|
4358
4705
|
initialDiscoveryError = error;
|
|
4359
4706
|
}
|
|
4360
4707
|
discoverySource = resolved?.discoverySource || 'supabase';
|
|
4361
4708
|
if (resolved?.discoverySource === 'cache') {
|
|
4362
4709
|
writeSavedSessionToFile(session);
|
|
4363
|
-
console.log(resolved.connectionTransport === 'relay-fallback'
|
|
4364
|
-
? `Fresh Hub registry lookup was unavailable; using saved
|
|
4365
|
-
: `Fresh Hub registry lookup was unavailable; using the responding saved direct Hub endpoint ${resolved.manager}.`);
|
|
4710
|
+
console.log(resolved.connectionTransport === 'relay-fallback'
|
|
4711
|
+
? `Fresh Hub registry lookup was unavailable; using saved workspace-bound Hub identity ${resolved.hubDeviceId} for the bounded encrypted relay path.`
|
|
4712
|
+
: `Fresh Hub registry lookup was unavailable; using the responding saved direct Hub endpoint ${resolved.manager}.`);
|
|
4366
4713
|
}
|
|
4367
4714
|
choice.session = session;
|
|
4368
4715
|
const email = session?.user?.email ? ` as ${session.user.email}` : '';
|
|
4369
4716
|
console.log(`Signed in to VuvoDesk${email}.`);
|
|
4370
4717
|
if (!resolved) {
|
|
4371
|
-
resolved = await waitForManagerFromSupabase(supabase, {
|
|
4372
|
-
allowRelayFallback,
|
|
4373
|
-
initialError: initialDiscoveryError,
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4718
|
+
resolved = await waitForManagerFromSupabase(supabase, {
|
|
4719
|
+
allowRelayFallback,
|
|
4720
|
+
initialError: initialDiscoveryError,
|
|
4721
|
+
workspaceId,
|
|
4722
|
+
deviceId: parsed.deviceId,
|
|
4723
|
+
memberUserId: session?.user?.id,
|
|
4724
|
+
shouldStop: () => Boolean(roleRestartRequest?.role)
|
|
4725
|
+
});
|
|
4726
|
+
discoverySource = 'supabase';
|
|
4727
|
+
}
|
|
4378
4728
|
if (!resolved && roleRestartRequest?.role) {
|
|
4379
4729
|
return {
|
|
4380
4730
|
...parsed,
|
|
@@ -4386,15 +4736,18 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
|
|
|
4386
4736
|
rediscoverOnDisconnect: shouldLogin,
|
|
4387
4737
|
rediscoverOnInvalidPair: shouldLogin
|
|
4388
4738
|
};
|
|
4389
|
-
}
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4739
|
+
}
|
|
4740
|
+
workspaceId = normalizeWorkspaceId(resolved.workspaceId) || workspaceId;
|
|
4741
|
+
cacheOwnerKey = hubCacheOwnerForSession(session, workspaceId) || cacheOwnerKey;
|
|
4742
|
+
if (discoverySource === 'supabase') {
|
|
4743
|
+
writeCachedHubTarget(cacheOwnerKey, resolved);
|
|
4744
|
+
const registration = await registerClientDeviceWithSupabase(supabase, {
|
|
4745
|
+
deviceId: parsed.deviceId,
|
|
4746
|
+
deviceName: os.hostname(),
|
|
4747
|
+
assignedHubId: resolved.hubDeviceId,
|
|
4748
|
+
workspaceId,
|
|
4749
|
+
session
|
|
4750
|
+
});
|
|
4398
4751
|
if (Number.isInteger(registration?.role_version)) {
|
|
4399
4752
|
connectionPage?.update({ roleVersion: registration.role_version });
|
|
4400
4753
|
}
|