@livedesk/client 0.1.229 → 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.
@@ -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
- return createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
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.rpc('set_livedesk_device_role', {
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.rpc('set_livedesk_device_role', {
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,38 @@ 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
- 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
+ }
4078
4178
  if (options.signal?.aborted) {
4079
4179
  throw createHubDiscoveryError('hub-registry-aborted', 'LiveDesk Hub registry lookup was cancelled.');
4080
4180
  }
4081
- let query = supabase
4082
- .from('livedesk_remote_host_targets')
4083
- .select('node_id, endpoint, endpoint_candidates, pair_token, active, expires_at, updated_at, manager_version')
4084
- .eq('product_key', 'livedesk');
4085
- if (options.signal && typeof query.abortSignal === 'function') {
4086
- query = query.abortSignal(options.signal);
4087
- }
4088
- const { data, error } = await query.maybeSingle();
4089
- if (error) {
4090
- throw error;
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;
4091
4206
  }
4092
4207
  if (!data?.active) {
4093
4208
  throw createHubDiscoveryError(
@@ -4148,7 +4263,7 @@ async function registerClientDeviceWithSupabase(supabase, options = {}) {
4148
4263
  if (roleRestartRequest?.role) {
4149
4264
  return { data: { ok: false, skipped: true, reason: 'role-transition-pending' }, error: null };
4150
4265
  }
4151
- return supabase.rpc('register_livedesk_device', {
4266
+ return callVerifiedSupabaseRpc(supabase, options.session, 'register_livedesk_device', {
4152
4267
  p_device_id: options.deviceId,
4153
4268
  p_device_name: options.deviceName || os.hostname(),
4154
4269
  p_role: 'client',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livedesk/client",
3
- "version": "0.1.229",
3
+ "version": "0.1.230",
4
4
  "description": "LiveDesk local remote client",
5
5
  "type": "module",
6
6
  "bin": {