@livedesk/client 0.1.228 → 0.1.230
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 +134 -23
- 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) {
|
|
@@ -1383,14 +1480,10 @@ async function activateSupabaseSession(supabase, session) {
|
|
|
1383
1480
|
if (!normalized.ok) {
|
|
1384
1481
|
throw new Error(normalized.error);
|
|
1385
1482
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
throw new Error('refresh-token-required');
|
|
1391
|
-
}
|
|
1392
|
-
return activeSession;
|
|
1393
|
-
}
|
|
1483
|
+
// Always install the verified session into this exact Supabase client.
|
|
1484
|
+
// A freshly created client can read the persisted token through getSession()
|
|
1485
|
+
// before PostgREST has adopted its Authorization header. Skipping setSession
|
|
1486
|
+
// in that state makes the UI look signed in while table and RPC calls run as anon.
|
|
1394
1487
|
const { data, error } = await supabase.auth.setSession({
|
|
1395
1488
|
access_token: normalized.session.access_token,
|
|
1396
1489
|
refresh_token: normalized.session.refresh_token
|
|
@@ -3152,7 +3245,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
3152
3245
|
dashboardState.choice?.session || savedSession
|
|
3153
3246
|
);
|
|
3154
3247
|
const expectedRoleVersion = Number(dashboardState.roleVersion || 0);
|
|
3155
|
-
const { data, error } = await enqueueDeviceRoleMutation(() => supabase
|
|
3248
|
+
const { data, error } = await enqueueDeviceRoleMutation(() => callVerifiedSupabaseRpc(supabase, session, 'set_livedesk_device_role', {
|
|
3156
3249
|
p_device_id: deviceId,
|
|
3157
3250
|
p_role: 'hub',
|
|
3158
3251
|
p_assigned_hub_id: null,
|
|
@@ -3498,7 +3591,7 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3498
3591
|
};
|
|
3499
3592
|
}
|
|
3500
3593
|
const expectedRoleVersion = Number(snapshot?.roleVersion || 0);
|
|
3501
|
-
const { data, error } = await enqueueDeviceRoleMutation(() => activeSupabase
|
|
3594
|
+
const { data, error } = await enqueueDeviceRoleMutation(() => callVerifiedSupabaseRpc(activeSupabase, session, 'set_livedesk_device_role', {
|
|
3502
3595
|
p_device_id: options.deviceId,
|
|
3503
3596
|
p_role: role,
|
|
3504
3597
|
p_assigned_hub_id: null,
|
|
@@ -4078,20 +4171,38 @@ export async function resolveManagerFromSupabase(supabase, options = {}) {
|
|
|
4078
4171
|
if (options.signal?.aborted) {
|
|
4079
4172
|
throw createHubDiscoveryError('hub-registry-aborted', 'LiveDesk Hub registry lookup was cancelled.');
|
|
4080
4173
|
}
|
|
4081
|
-
await refreshSessionIfNeeded(supabase);
|
|
4174
|
+
const session = await refreshSessionIfNeeded(supabase);
|
|
4175
|
+
if (!session?.access_token) {
|
|
4176
|
+
throw createHubDiscoveryError('hub-registry-auth-required', 'Sign in again before finding the LiveDesk Hub.');
|
|
4177
|
+
}
|
|
4082
4178
|
if (options.signal?.aborted) {
|
|
4083
4179
|
throw createHubDiscoveryError('hub-registry-aborted', 'LiveDesk Hub registry lookup was cancelled.');
|
|
4084
4180
|
}
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4181
|
+
const fetchImpl = explicitAuthenticatedFetch(supabase, options.fetchImpl);
|
|
4182
|
+
let data;
|
|
4183
|
+
if (fetchImpl) {
|
|
4184
|
+
const payload = await fetchAuthenticatedSupabaseJson(session, 'livedesk_remote_host_targets', {
|
|
4185
|
+
fetchImpl,
|
|
4186
|
+
signal: options.signal,
|
|
4187
|
+
query: {
|
|
4188
|
+
select: 'node_id,endpoint,endpoint_candidates,pair_token,active,expires_at,updated_at,manager_version',
|
|
4189
|
+
product_key: 'eq.livedesk',
|
|
4190
|
+
limit: '1'
|
|
4191
|
+
},
|
|
4192
|
+
errorMessage: 'LiveDesk could not read the current Hub registration.'
|
|
4193
|
+
});
|
|
4194
|
+
data = Array.isArray(payload) ? payload[0] : payload;
|
|
4195
|
+
} else {
|
|
4196
|
+
let query = supabase
|
|
4197
|
+
.from('livedesk_remote_host_targets')
|
|
4198
|
+
.select('node_id, endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
|
|
4199
|
+
.eq('product_key', 'livedesk');
|
|
4200
|
+
if (options.signal && typeof query.abortSignal === 'function') {
|
|
4201
|
+
query = query.abortSignal(options.signal);
|
|
4202
|
+
}
|
|
4203
|
+
const result = await query.maybeSingle();
|
|
4204
|
+
if (result.error) throw result.error;
|
|
4205
|
+
data = result.data;
|
|
4095
4206
|
}
|
|
4096
4207
|
if (!data?.active) {
|
|
4097
4208
|
throw createHubDiscoveryError(
|
|
@@ -4152,7 +4263,7 @@ async function registerClientDeviceWithSupabase(supabase, options = {}) {
|
|
|
4152
4263
|
if (roleRestartRequest?.role) {
|
|
4153
4264
|
return { data: { ok: false, skipped: true, reason: 'role-transition-pending' }, error: null };
|
|
4154
4265
|
}
|
|
4155
|
-
return supabase.
|
|
4266
|
+
return callVerifiedSupabaseRpc(supabase, options.session, 'register_livedesk_device', {
|
|
4156
4267
|
p_device_id: options.deviceId,
|
|
4157
4268
|
p_device_name: options.deviceName || os.hostname(),
|
|
4158
4269
|
p_role: 'client',
|