@xenide-io/suite-client 0.2.0 → 0.4.0

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/dist/index.mjs CHANGED
@@ -141,6 +141,7 @@ function createApiClient(config) {
141
141
  const ACCESS_KEY = config.accessTokenKey ?? `${config.storagePrefix}_access_token`;
142
142
  const REFRESH_KEY = config.refreshTokenKey ?? `${config.storagePrefix}_refresh_token`;
143
143
  const WORKSPACE_KEY = config.workspaceKey ?? `${config.storagePrefix}_active_workspace_id`;
144
+ const CLEAR_WORKSPACE = config.clearWorkspaceOnSignOut ?? true;
144
145
  let accessToken = null;
145
146
  let refreshToken = null;
146
147
  let activeWorkspaceId = null;
@@ -178,10 +179,12 @@ function createApiClient(config) {
178
179
  }
179
180
  function setTokens(access, refresh) {
180
181
  accessToken = access;
181
- refreshToken = refresh;
182
+ authFailureFired = false;
183
+ if (refresh !== void 0) refreshToken = refresh;
182
184
  if (typeof window !== "undefined") {
183
185
  window.localStorage.setItem(ACCESS_KEY, access);
184
- window.localStorage.setItem(REFRESH_KEY, refresh);
186
+ if (refresh) window.localStorage.setItem(REFRESH_KEY, refresh);
187
+ else if (refresh !== void 0) window.localStorage.removeItem(REFRESH_KEY);
185
188
  }
186
189
  }
187
190
  function getAccessToken() {
@@ -198,6 +201,9 @@ function createApiClient(config) {
198
201
  }
199
202
  return refreshToken;
200
203
  }
204
+ function hasAccessToken() {
205
+ return Boolean(getAccessToken());
206
+ }
201
207
  function setActiveWorkspaceId(id, options) {
202
208
  const changed = activeWorkspaceId !== id;
203
209
  activeWorkspaceId = id;
@@ -224,13 +230,17 @@ function createApiClient(config) {
224
230
  }
225
231
  }
226
232
  function clearAuth() {
233
+ config.onSignOut?.();
227
234
  accessToken = null;
228
235
  refreshToken = null;
229
236
  activeWorkspaceId = null;
230
237
  if (typeof window !== "undefined") {
231
238
  window.localStorage.removeItem(ACCESS_KEY);
232
239
  window.localStorage.removeItem(REFRESH_KEY);
233
- window.localStorage.removeItem(WORKSPACE_KEY);
240
+ if (CLEAR_WORKSPACE) window.localStorage.removeItem(WORKSPACE_KEY);
241
+ for (const key of config.extraStorageKeysOnClear ?? []) {
242
+ window.localStorage.removeItem(key);
243
+ }
234
244
  const prefixes = config.sessionStorageClearPrefixes ?? [];
235
245
  if (prefixes.length > 0) {
236
246
  try {
@@ -289,36 +299,43 @@ function createApiClient(config) {
289
299
  return false;
290
300
  }
291
301
  }
292
- async function apiFetch(path, options = {}) {
302
+ async function apiFetch(path, init = {}) {
303
+ const { skipAuth, ...requestInit } = init;
293
304
  const token = getAccessToken();
294
305
  const headers = {
295
306
  Accept: "application/json",
296
- ...options.headers
307
+ ...requestInit.headers
297
308
  };
298
- const body = options.body;
309
+ const body = requestInit.body;
299
310
  const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
300
311
  if (!isFormData) {
301
312
  headers["Content-Type"] = "application/json";
302
313
  }
303
- if (token) {
314
+ if (token && !skipAuth) {
304
315
  headers["Authorization"] = `Bearer ${token}`;
305
316
  }
306
317
  const wsId = getActiveWorkspaceId();
307
- if (wsId) {
318
+ if (wsId && !skipAuth) {
308
319
  headers["X-Active-Workspace-Id"] = wsId;
309
320
  }
310
321
  const url = path.startsWith("http") ? path : `${API_URL}${path}`;
322
+ if (config.enforceSameOrigin && !skipAuth && path.startsWith("http") && !url.startsWith(API_URL)) {
323
+ throw new Error("Refusing to send the session to an unexpected origin.");
324
+ }
311
325
  async function doFetch() {
312
326
  try {
313
- return await fetch(url, { ...options, headers });
314
- } catch {
327
+ return await fetch(url, { ...requestInit, headers });
328
+ } catch (error) {
329
+ if (init.signal?.aborted || error instanceof DOMException && error.name === "AbortError") {
330
+ throw error;
331
+ }
315
332
  throw new Error(
316
333
  `Could not reach the API at ${API_URL}. Is the Django backend running?`
317
334
  );
318
335
  }
319
336
  }
320
337
  let res = await doFetch();
321
- if (res.status === 401 && token) {
338
+ if (res.status === 401 && token && !skipAuth) {
322
339
  const refreshed = await refreshAccessToken();
323
340
  if (refreshed) {
324
341
  const newToken = getAccessToken();
@@ -326,9 +343,10 @@ function createApiClient(config) {
326
343
  headers["Authorization"] = `Bearer ${newToken}`;
327
344
  }
328
345
  res = await doFetch();
329
- if (res.status === 401) {
330
- triggerAuthFailure();
331
- }
346
+ }
347
+ if (res.status === 401) {
348
+ clearAuth();
349
+ triggerAuthFailure();
332
350
  }
333
351
  }
334
352
  return res;
@@ -337,21 +355,23 @@ function createApiClient(config) {
337
355
  return formatAppError(await appErrorFromResponse(res));
338
356
  }
339
357
  const api = {
340
- get: (path) => apiFetch(path),
341
- post: (path, body, options = {}) => apiFetch(path, {
342
- ...options,
358
+ get: (path, init) => apiFetch(path, init),
359
+ post: (path, body, init = {}) => apiFetch(path, {
360
+ ...init,
343
361
  method: "POST",
344
362
  body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0
345
363
  }),
346
- put: (path, body) => apiFetch(path, {
364
+ put: (path, body, init = {}) => apiFetch(path, {
365
+ ...init,
347
366
  method: "PUT",
348
367
  body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0
349
368
  }),
350
- patch: (path, body) => apiFetch(path, {
369
+ patch: (path, body, init = {}) => apiFetch(path, {
370
+ ...init,
351
371
  method: "PATCH",
352
372
  body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0
353
373
  }),
354
- delete: (path) => apiFetch(path, { method: "DELETE" })
374
+ delete: (path, init = {}) => apiFetch(path, { ...init, method: "DELETE" })
355
375
  };
356
376
  return {
357
377
  API_URL,
@@ -359,6 +379,7 @@ function createApiClient(config) {
359
379
  setTokens,
360
380
  getAccessToken,
361
381
  getRefreshToken,
382
+ hasAccessToken,
362
383
  setActiveWorkspaceId,
363
384
  getActiveWorkspaceId,
364
385
  restoreWorkspace,
@@ -366,7 +387,10 @@ function createApiClient(config) {
366
387
  restoreTokens,
367
388
  refreshAccessToken,
368
389
  parseApiError,
369
- api
390
+ api,
391
+ // Aliases used by Kraken/Shelly.
392
+ restoreSession: restoreTokens,
393
+ clearSession: clearAuth
370
394
  };
371
395
  }
372
396
 
@@ -958,6 +982,105 @@ function createNotificationsClient(config) {
958
982
  };
959
983
  }
960
984
 
985
+ // src/notifications/create-web-push-client.ts
986
+ function createWebPushClient(config) {
987
+ const { api, parseApiError } = config;
988
+ const PUBLIC_KEY_PATH = config.publicKeyPath ?? "/api/notifications/push/public-key/";
989
+ const SUBSCRIBE_PATH = config.subscribePath ?? "/api/notifications/push/subscribe/";
990
+ const UNSUBSCRIBE_PATH = config.unsubscribePath ?? "/api/notifications/push/unsubscribe/";
991
+ const SW_PATH = config.serviceWorkerPath ?? "/sw.js";
992
+ const SW_SCOPE = config.serviceWorkerScope ?? "/";
993
+ function supported() {
994
+ return typeof window !== "undefined" && "serviceWorker" in navigator && "PushManager" in window && "Notification" in window;
995
+ }
996
+ async function fetchConfig() {
997
+ const res = await api.get(PUBLIC_KEY_PATH);
998
+ if (!res.ok) throw new Error(await parseApiError(res));
999
+ return await res.json();
1000
+ }
1001
+ async function getRegistration() {
1002
+ const existing = await navigator.serviceWorker.getRegistration(SW_SCOPE);
1003
+ return existing ?? null;
1004
+ }
1005
+ function urlBase64ToUint8Array(base64) {
1006
+ const padding = "=".repeat((4 - base64.length % 4) % 4);
1007
+ const normalized = (base64 + padding).replace(/-/g, "+").replace(/_/g, "/");
1008
+ const raw = atob(normalized);
1009
+ const output = new Uint8Array(raw.length);
1010
+ for (let i = 0; i < raw.length; i += 1) {
1011
+ output[i] = raw.charCodeAt(i);
1012
+ }
1013
+ return output;
1014
+ }
1015
+ async function getState() {
1016
+ if (!supported()) return "unsupported";
1017
+ let cfg;
1018
+ try {
1019
+ cfg = await fetchConfig();
1020
+ } catch {
1021
+ return "unconfigured";
1022
+ }
1023
+ if (!cfg.enabled || !cfg.public_key) return "unconfigured";
1024
+ if (Notification.permission === "denied") return "denied";
1025
+ const registration = await getRegistration();
1026
+ const subscription = await registration?.pushManager.getSubscription();
1027
+ if (subscription && Notification.permission === "granted") {
1028
+ return "subscribed";
1029
+ }
1030
+ if (Notification.permission === "granted") return "granted";
1031
+ return "default";
1032
+ }
1033
+ async function enable() {
1034
+ if (!supported()) {
1035
+ throw new Error("This browser does not support notifications.");
1036
+ }
1037
+ const permission = await Notification.requestPermission();
1038
+ if (permission !== "granted") {
1039
+ throw new Error("Notification permission was not granted.");
1040
+ }
1041
+ const cfg = await fetchConfig();
1042
+ if (!cfg.enabled || !cfg.public_key) {
1043
+ throw new Error("Push notifications are not configured on the server.");
1044
+ }
1045
+ let registration = await getRegistration();
1046
+ if (!registration) {
1047
+ registration = await navigator.serviceWorker.register(SW_PATH, {
1048
+ scope: SW_SCOPE
1049
+ });
1050
+ }
1051
+ await navigator.serviceWorker.ready;
1052
+ const existing = await registration.pushManager.getSubscription();
1053
+ const subscription = existing ?? await registration.pushManager.subscribe({
1054
+ userVisibleOnly: true,
1055
+ applicationServerKey: urlBase64ToUint8Array(cfg.public_key)
1056
+ });
1057
+ const json = subscription.toJSON();
1058
+ if (!json.endpoint || !json.keys?.p256dh || !json.keys?.auth) {
1059
+ throw new Error("The browser returned an incomplete push subscription.");
1060
+ }
1061
+ const res = await api.post(SUBSCRIBE_PATH, {
1062
+ endpoint: json.endpoint,
1063
+ keys: { p256dh: json.keys.p256dh, auth: json.keys.auth },
1064
+ user_agent: typeof navigator !== "undefined" ? navigator.userAgent : ""
1065
+ });
1066
+ if (!res.ok) throw new Error(await parseApiError(res));
1067
+ }
1068
+ async function disable() {
1069
+ if (!supported()) return;
1070
+ const registration = await getRegistration();
1071
+ const subscription = await registration?.pushManager.getSubscription();
1072
+ if (!subscription) return;
1073
+ const endpoint = subscription.endpoint;
1074
+ try {
1075
+ await subscription.unsubscribe();
1076
+ } catch {
1077
+ }
1078
+ const res = await api.post(UNSUBSCRIBE_PATH, { endpoint });
1079
+ if (!res.ok) throw new Error(await parseApiError(res));
1080
+ }
1081
+ return { supported, getState, enable, disable };
1082
+ }
1083
+
961
1084
  // src/ai/suite-ai-job.ts
962
1085
  function activeSuiteAiJobs(jobs) {
963
1086
  return jobs.filter(
@@ -1085,6 +1208,7 @@ export {
1085
1208
  createJsonLd,
1086
1209
  createLiveRefresh,
1087
1210
  createNotificationsClient,
1211
+ createWebPushClient,
1088
1212
  createWorkspaceAppearance,
1089
1213
  createWorkspaceAppearanceApi,
1090
1214
  defaultInviteSeatType,