@livedesk/client 0.1.229 → 0.1.231
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 +151 -16
- package/package.json +1 -1
package/bin/livedesk-client.js
CHANGED
|
@@ -1154,9 +1154,99 @@ export async function fetchSupabaseWithDeadline(
|
|
|
1154
1154
|
}
|
|
1155
1155
|
}
|
|
1156
1156
|
|
|
1157
|
+
const LIVEDESK_AUTHENTICATED_FETCH = '__liveDeskAuthenticatedFetch';
|
|
1158
|
+
|
|
1159
|
+
function decodeSupabaseJwtRole(accessToken) {
|
|
1160
|
+
try {
|
|
1161
|
+
const parts = String(accessToken || '').split('.');
|
|
1162
|
+
if (parts.length !== 3) return '';
|
|
1163
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
1164
|
+
return String(payload?.role || '').trim().toLowerCase();
|
|
1165
|
+
} catch {
|
|
1166
|
+
return '';
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function authenticatedSupabaseError(response, payload, fallback) {
|
|
1171
|
+
const message = String(
|
|
1172
|
+
payload?.message
|
|
1173
|
+
|| payload?.error_description
|
|
1174
|
+
|| payload?.error
|
|
1175
|
+
|| payload?.details
|
|
1176
|
+
|| fallback
|
|
1177
|
+
|| `Supabase request failed with HTTP ${response.status}.`
|
|
1178
|
+
).trim();
|
|
1179
|
+
const error = new Error(message);
|
|
1180
|
+
error.code = String(payload?.code || '').trim();
|
|
1181
|
+
error.status = Number(response.status || 0);
|
|
1182
|
+
error.details = String(payload?.details || '').trim();
|
|
1183
|
+
error.hint = String(payload?.hint || '').trim();
|
|
1184
|
+
return error;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
export async function fetchAuthenticatedSupabaseJson(session, pathname, options = {}) {
|
|
1188
|
+
const normalized = normalizeRuntimeAuthSession(session);
|
|
1189
|
+
if (!normalized.ok) throw new Error(normalized.error);
|
|
1190
|
+
const role = decodeSupabaseJwtRole(normalized.session.access_token);
|
|
1191
|
+
if (role && role !== 'authenticated') {
|
|
1192
|
+
throw new Error(`LiveDesk sign-in token has unexpected database role ${role}. Sign out and sign in again.`);
|
|
1193
|
+
}
|
|
1194
|
+
const cleanPath = String(pathname || '').replace(/^\/+/, '');
|
|
1195
|
+
if (!cleanPath || cleanPath.includes('..')) throw new Error('invalid-supabase-rest-path');
|
|
1196
|
+
const url = new URL(`${SUPABASE_URL.replace(/\/+$/, '')}/rest/v1/${cleanPath}`);
|
|
1197
|
+
for (const [key, value] of Object.entries(options.query || {})) {
|
|
1198
|
+
if (value === undefined || value === null || value === '') continue;
|
|
1199
|
+
url.searchParams.set(key, String(value));
|
|
1200
|
+
}
|
|
1201
|
+
const headers = new Headers(options.headers || {});
|
|
1202
|
+
headers.set('apikey', SUPABASE_PUBLISHABLE_KEY);
|
|
1203
|
+
headers.set('Authorization', `Bearer ${normalized.session.access_token}`);
|
|
1204
|
+
headers.set('Accept', 'application/json');
|
|
1205
|
+
const method = String(options.method || 'GET').toUpperCase();
|
|
1206
|
+
let body;
|
|
1207
|
+
if (options.body !== undefined) {
|
|
1208
|
+
headers.set('Content-Type', 'application/json');
|
|
1209
|
+
body = JSON.stringify(options.body);
|
|
1210
|
+
}
|
|
1211
|
+
const fetchImpl = typeof options.fetchImpl === 'function'
|
|
1212
|
+
? options.fetchImpl
|
|
1213
|
+
: fetchSupabaseWithDeadline;
|
|
1214
|
+
const response = await fetchImpl(url, { method, headers, body, signal: options.signal });
|
|
1215
|
+
const text = await response.text();
|
|
1216
|
+
let payload = null;
|
|
1217
|
+
if (text) {
|
|
1218
|
+
try { payload = JSON.parse(text); }
|
|
1219
|
+
catch { payload = { message: text }; }
|
|
1220
|
+
}
|
|
1221
|
+
if (!response.ok) throw authenticatedSupabaseError(response, payload, options.errorMessage);
|
|
1222
|
+
return payload;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function explicitAuthenticatedFetch(supabase, override = null) {
|
|
1226
|
+
if (typeof override === 'function') return override;
|
|
1227
|
+
return typeof supabase?.[LIVEDESK_AUTHENTICATED_FETCH] === 'function'
|
|
1228
|
+
? supabase[LIVEDESK_AUTHENTICATED_FETCH]
|
|
1229
|
+
: null;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
export async function callVerifiedSupabaseRpc(supabase, session, functionName, args = {}, options = {}) {
|
|
1233
|
+
const name = String(functionName || '').trim();
|
|
1234
|
+
if (!/^[a-z0-9_]+$/u.test(name)) throw new Error('invalid-supabase-rpc-name');
|
|
1235
|
+
const fetchImpl = explicitAuthenticatedFetch(supabase, options.fetchImpl);
|
|
1236
|
+
if (!fetchImpl) return supabase.rpc(name, args);
|
|
1237
|
+
const data = await fetchAuthenticatedSupabaseJson(session, `rpc/${name}`, {
|
|
1238
|
+
method: 'POST',
|
|
1239
|
+
body: args,
|
|
1240
|
+
fetchImpl,
|
|
1241
|
+
signal: options.signal,
|
|
1242
|
+
errorMessage: `${name} failed.`
|
|
1243
|
+
});
|
|
1244
|
+
return { data, error: null };
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1157
1247
|
async function createSupabaseClient() {
|
|
1158
1248
|
const { createClient } = await import('@supabase/supabase-js');
|
|
1159
|
-
|
|
1249
|
+
const client = createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
|
|
1160
1250
|
global: {
|
|
1161
1251
|
fetch: fetchSupabaseWithDeadline
|
|
1162
1252
|
},
|
|
@@ -1169,6 +1259,13 @@ async function createSupabaseClient() {
|
|
|
1169
1259
|
storage: createFileStorage(preferredClientAuthPath())
|
|
1170
1260
|
}
|
|
1171
1261
|
});
|
|
1262
|
+
Object.defineProperty(client, LIVEDESK_AUTHENTICATED_FETCH, {
|
|
1263
|
+
value: fetchSupabaseWithDeadline,
|
|
1264
|
+
enumerable: false,
|
|
1265
|
+
configurable: false,
|
|
1266
|
+
writable: false
|
|
1267
|
+
});
|
|
1268
|
+
return client;
|
|
1172
1269
|
}
|
|
1173
1270
|
|
|
1174
1271
|
function getNestedErrorCode(error) {
|
|
@@ -3148,7 +3245,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
3148
3245
|
dashboardState.choice?.session || savedSession
|
|
3149
3246
|
);
|
|
3150
3247
|
const expectedRoleVersion = Number(dashboardState.roleVersion || 0);
|
|
3151
|
-
const { data, error } = await enqueueDeviceRoleMutation(() => supabase
|
|
3248
|
+
const { data, error } = await enqueueDeviceRoleMutation(() => callVerifiedSupabaseRpc(supabase, session, 'set_livedesk_device_role', {
|
|
3152
3249
|
p_device_id: deviceId,
|
|
3153
3250
|
p_role: 'hub',
|
|
3154
3251
|
p_assigned_hub_id: null,
|
|
@@ -3494,7 +3591,7 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3494
3591
|
};
|
|
3495
3592
|
}
|
|
3496
3593
|
const expectedRoleVersion = Number(snapshot?.roleVersion || 0);
|
|
3497
|
-
const { data, error } = await enqueueDeviceRoleMutation(() => activeSupabase
|
|
3594
|
+
const { data, error } = await enqueueDeviceRoleMutation(() => callVerifiedSupabaseRpc(activeSupabase, session, 'set_livedesk_device_role', {
|
|
3498
3595
|
p_device_id: options.deviceId,
|
|
3499
3596
|
p_role: role,
|
|
3500
3597
|
p_assigned_hub_id: null,
|
|
@@ -4074,20 +4171,45 @@ export async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
|
4074
4171
|
if (options.signal?.aborted) {
|
|
4075
4172
|
throw createHubDiscoveryError('hub-registry-aborted', 'LiveDesk Hub registry lookup was cancelled.');
|
|
4076
4173
|
}
|
|
4077
|
-
|
|
4174
|
+
const fetchImpl = explicitAuthenticatedFetch(supabase, options.fetchImpl);
|
|
4175
|
+
const supplied = normalizeRuntimeAuthSession(options.session, { requireRefreshToken: true });
|
|
4176
|
+
const suppliedExpiresAt = Number(supplied.session?.expires_at || 0);
|
|
4177
|
+
const suppliedFresh = supplied.ok && (suppliedExpiresAt <= 0
|
|
4178
|
+
|| suppliedExpiresAt - Math.floor(Date.now() / 1000) > SESSION_REFRESH_SKEW_SECONDS);
|
|
4179
|
+
let session = suppliedFresh ? supplied.session : await refreshSessionIfNeeded(supabase);
|
|
4180
|
+
if (!session?.access_token) {
|
|
4181
|
+
throw createHubDiscoveryError('hub-registry-auth-required', 'Sign in again before finding the LiveDesk Hub.');
|
|
4182
|
+
}
|
|
4183
|
+
if (!fetchImpl && suppliedFresh) {
|
|
4184
|
+
session = await activateSupabaseSession(supabase, session);
|
|
4185
|
+
}
|
|
4078
4186
|
if (options.signal?.aborted) {
|
|
4079
4187
|
throw createHubDiscoveryError('hub-registry-aborted', 'LiveDesk Hub registry lookup was cancelled.');
|
|
4080
4188
|
}
|
|
4081
|
-
let
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4189
|
+
let data;
|
|
4190
|
+
if (fetchImpl) {
|
|
4191
|
+
const payload = await fetchAuthenticatedSupabaseJson(session, 'livedesk_remote_host_targets', {
|
|
4192
|
+
fetchImpl,
|
|
4193
|
+
signal: options.signal,
|
|
4194
|
+
query: {
|
|
4195
|
+
select: 'node_id,endpoint,endpoint_candidates,pair_token,active,expires_at,updated_at,manager_version',
|
|
4196
|
+
product_key: 'eq.livedesk',
|
|
4197
|
+
limit: '1'
|
|
4198
|
+
},
|
|
4199
|
+
errorMessage: 'LiveDesk could not read the current Hub registration.'
|
|
4200
|
+
});
|
|
4201
|
+
data = Array.isArray(payload) ? payload[0] : payload;
|
|
4202
|
+
} else {
|
|
4203
|
+
let query = supabase
|
|
4204
|
+
.from('livedesk_remote_host_targets')
|
|
4205
|
+
.select('node_id, endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
|
|
4206
|
+
.eq('product_key', 'livedesk');
|
|
4207
|
+
if (options.signal && typeof query.abortSignal === 'function') {
|
|
4208
|
+
query = query.abortSignal(options.signal);
|
|
4209
|
+
}
|
|
4210
|
+
const result = await query.maybeSingle();
|
|
4211
|
+
if (result.error) throw result.error;
|
|
4212
|
+
data = result.data;
|
|
4091
4213
|
}
|
|
4092
4214
|
if (!data?.active) {
|
|
4093
4215
|
throw createHubDiscoveryError(
|
|
@@ -4148,7 +4270,7 @@ async function registerClientDeviceWithSupabase(supabase, options = {}) {
|
|
|
4148
4270
|
if (roleRestartRequest?.role) {
|
|
4149
4271
|
return { data: { ok: false, skipped: true, reason: 'role-transition-pending' }, error: null };
|
|
4150
4272
|
}
|
|
4151
|
-
return supabase.
|
|
4273
|
+
return callVerifiedSupabaseRpc(supabase, options.session, 'register_livedesk_device', {
|
|
4152
4274
|
p_device_id: options.deviceId,
|
|
4153
4275
|
p_device_name: options.deviceName || os.hostname(),
|
|
4154
4276
|
p_role: 'client',
|
|
@@ -4182,6 +4304,7 @@ export async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
4182
4304
|
let lastMessage = '';
|
|
4183
4305
|
let wakeListener = null;
|
|
4184
4306
|
let initialError = options.initialError || null;
|
|
4307
|
+
let discoverySession = options.session || null;
|
|
4185
4308
|
console.log('Waiting for a LiveDesk Hub. LiveDesk is listening for Hub-online events with adaptive registry retries as a fallback.');
|
|
4186
4309
|
try {
|
|
4187
4310
|
while (true) {
|
|
@@ -4191,7 +4314,16 @@ export async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
4191
4314
|
if (!wakeListener) {
|
|
4192
4315
|
try {
|
|
4193
4316
|
wakeListener = await createWakeListener({
|
|
4194
|
-
getAccessToken: async () =>
|
|
4317
|
+
getAccessToken: async () => {
|
|
4318
|
+
const normalized = normalizeRuntimeAuthSession(discoverySession, { requireRefreshToken: true });
|
|
4319
|
+
const expiresAt = Number(normalized.session?.expires_at || 0);
|
|
4320
|
+
if (normalized.ok && (expiresAt <= 0
|
|
4321
|
+
|| expiresAt - Math.floor(Date.now() / 1000) > SESSION_REFRESH_SKEW_SECONDS)) {
|
|
4322
|
+
return normalized.session.access_token;
|
|
4323
|
+
}
|
|
4324
|
+
discoverySession = await refreshSessionIfNeeded(supabase);
|
|
4325
|
+
return discoverySession?.access_token || '';
|
|
4326
|
+
}
|
|
4195
4327
|
});
|
|
4196
4328
|
} catch (error) {
|
|
4197
4329
|
if (attempts === 0) {
|
|
@@ -4210,6 +4342,7 @@ export async function waitForManagerFromSupabase(supabase, options = {}) {
|
|
|
4210
4342
|
signal => resolveManagerFromSupabase(supabase, {
|
|
4211
4343
|
allowRelayFallback: options.allowRelayFallback === true,
|
|
4212
4344
|
probeEndpoint: options.probeEndpoint,
|
|
4345
|
+
session: discoverySession,
|
|
4213
4346
|
signal
|
|
4214
4347
|
}),
|
|
4215
4348
|
{
|
|
@@ -4380,6 +4513,7 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
|
|
|
4380
4513
|
}
|
|
4381
4514
|
return await resolveManagerFromSupabase(supabase, {
|
|
4382
4515
|
allowRelayFallback,
|
|
4516
|
+
session,
|
|
4383
4517
|
signal
|
|
4384
4518
|
});
|
|
4385
4519
|
}
|
|
@@ -4412,6 +4546,7 @@ async function prepareLoginConnection(parsed, existingConnectionPage = null, get
|
|
|
4412
4546
|
if (!resolved) {
|
|
4413
4547
|
resolved = await waitForManagerFromSupabase(supabase, {
|
|
4414
4548
|
allowRelayFallback,
|
|
4549
|
+
session,
|
|
4415
4550
|
initialError: initialDiscoveryError,
|
|
4416
4551
|
shouldStop: () => Boolean(roleRestartRequest?.role)
|
|
4417
4552
|
});
|