@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.d.mts CHANGED
@@ -45,6 +45,9 @@ declare function normalizeThrown(input: unknown, capability?: string): AppError;
45
45
  /** Display string for a failure surface: capability copy plus the code. */
46
46
  declare function formatAppError(error: AppError): string;
47
47
 
48
+ type ApiFetchInit = RequestInit & {
49
+ skipAuth?: boolean;
50
+ };
48
51
  interface ApiClientConfig {
49
52
  /** localStorage key namespace, e.g. `tt` or `tides`. */
50
53
  storagePrefix: string;
@@ -61,10 +64,17 @@ interface ApiClientConfig {
61
64
  workspaceCookieDomain?: string;
62
65
  /** Fired when the active workspace changes in this tab. */
63
66
  onWorkspaceChange?: (id: string | null) => void;
67
+ /** Fired during sign-out before storage is cleared. */
68
+ onSignOut?: () => void;
69
+ /** Clear the workspace key on sign-out (default `true`). */
70
+ clearWorkspaceOnSignOut?: boolean;
71
+ /** Extra localStorage keys removed on sign-out (e.g. portal app tokens). */
72
+ extraStorageKeysOnClear?: string[];
64
73
  /** sessionStorage key prefixes to clear on sign-out. */
65
74
  sessionStorageClearPrefixes?: string[];
75
+ /** Refuse to send the session to an unexpected absolute origin. */
76
+ enforceSameOrigin?: boolean;
66
77
  }
67
- type ApiFetch = (path: string, options?: RequestInit) => Promise<Response>;
68
78
  /**
69
79
  * Build a browser API client scoped to one ShellStack app. Every app shares the
70
80
  * same fetch/refresh/workspace behaviour; only the storage namespace and
@@ -73,9 +83,10 @@ type ApiFetch = (path: string, options?: RequestInit) => Promise<Response>;
73
83
  declare function createApiClient(config: ApiClientConfig): {
74
84
  API_URL: string;
75
85
  onAuthFailure: (handler: () => void) => void;
76
- setTokens: (access: string, refresh: string) => void;
86
+ setTokens: (access: string, refresh?: string | null) => void;
77
87
  getAccessToken: () => string | null;
78
88
  getRefreshToken: () => string | null;
89
+ hasAccessToken: () => boolean;
79
90
  setActiveWorkspaceId: (id: string | null, options?: {
80
91
  share?: boolean;
81
92
  }) => void;
@@ -86,12 +97,14 @@ declare function createApiClient(config: ApiClientConfig): {
86
97
  refreshAccessToken: () => Promise<boolean>;
87
98
  parseApiError: (res: Response) => Promise<string>;
88
99
  api: {
89
- get: (path: string) => Promise<Response>;
90
- post: (path: string, body?: unknown, options?: RequestInit) => Promise<Response>;
91
- put: (path: string, body?: unknown) => Promise<Response>;
92
- patch: (path: string, body?: unknown) => Promise<Response>;
93
- delete: (path: string) => Promise<Response>;
100
+ get: (path: string, init?: ApiFetchInit) => Promise<Response>;
101
+ post: (path: string, body?: unknown, init?: ApiFetchInit) => Promise<Response>;
102
+ put: (path: string, body?: unknown, init?: ApiFetchInit) => Promise<Response>;
103
+ patch: (path: string, body?: unknown, init?: ApiFetchInit) => Promise<Response>;
104
+ delete: (path: string, init?: ApiFetchInit) => Promise<Response>;
94
105
  };
106
+ restoreSession: () => void;
107
+ clearSession: () => void;
95
108
  };
96
109
  type ApiClient = ReturnType<typeof createApiClient>;
97
110
  type Api = ApiClient['api'];
@@ -313,6 +326,36 @@ declare function createNotificationsClient(config: NotificationsClientConfig): {
313
326
  markAllSuiteNotificationsRead: () => Promise<void>;
314
327
  };
315
328
 
329
+ /** Browser push state surfaced to settings UI. */
330
+ type WebPushState = 'unsupported' | 'unconfigured' | 'default' | 'granted' | 'denied' | 'subscribed';
331
+ interface WebPushConfig {
332
+ enabled: boolean;
333
+ public_key: string;
334
+ }
335
+ interface WebPushClientConfig {
336
+ api: Api;
337
+ parseApiError: (res: Response) => Promise<string>;
338
+ publicKeyPath?: string;
339
+ subscribePath?: string;
340
+ unsubscribePath?: string;
341
+ serviceWorkerPath?: string;
342
+ serviceWorkerScope?: string;
343
+ }
344
+ type WebPushClient = ReturnType<typeof createWebPushClient>;
345
+ /**
346
+ * Browser Web Push client shared across ShellStack apps.
347
+ *
348
+ * Registration is per origin: each app ships its own `/sw.js` and manifest,
349
+ * but all talk to the same suite endpoints, so a subscription made in one app
350
+ * receives notifications for the whole suite.
351
+ */
352
+ declare function createWebPushClient(config: WebPushClientConfig): {
353
+ supported: () => boolean;
354
+ getState: () => Promise<WebPushState>;
355
+ enable: () => Promise<void>;
356
+ disable: () => Promise<void>;
357
+ };
358
+
316
359
  type SuiteAiJobEvent = {
317
360
  sequence: number;
318
361
  event_type: 'queued' | 'started' | 'progress' | 'tool' | 'completed' | 'failed' | 'cancelled';
@@ -373,4 +416,4 @@ declare function createJsonLd(config: JsonLdConfig): string;
373
416
 
374
417
  declare function useIsNight(): boolean;
375
418
 
376
- export { type Api, type ApiClient, type ApiClientConfig, type ApiFetch, type AppError, type AuthReturnPath, type AuthReturnPathConfig, type BrandSettings, type BrandSettingsConfig, type CrossAppSync, type CrossAppSyncConfig, type CrossAppSyncTopic, DEFAULT_ERROR_CODE, ERROR_CODES, ERROR_COPY, ERROR_RETRYABLE, type ErrorCode, FREE_PLAN_SEAT_HELP, type JsonLdConfig, type LiveRefresh, type LiveRefreshOptions, type NotificationsClient, type NotificationsClientConfig, ORGANISATION_SEAT_HELP, type PortalHandoffPayload, type PortalHandoffRead, type SeatType, type SubscriptionTier, type SuiteAiJob, type SuiteAiJobEvent, type SuiteNotification, type SuiteNotificationsResponse, TIDES_ACCESS_HELP, TURTLETIME_ACCESS_HELP, WEBHOOK_ACTIVITY_ACTIONS, WORKSPACE_SEAT_HELP, type WorkspaceAppearance, type WorkspaceAppearanceApiConfig, type WorkspaceAppearanceConfig, WorkspaceAppearanceError, type WorkspaceAppearanceHelpers, type WorkspaceAppearancePatch, type WorkspaceBrandPayload, type WorkspaceSubscription, type WorkspaceTheme, activeSuiteAiJobs, appErrorFromResponse, canDisableAppAccess, codeForStatus, copyFor, createApiClient, createAuthReturnPath, createBrandSettings, createCrossAppSync, createJsonLd, createLiveRefresh, createNotificationsClient, createWorkspaceAppearance, createWorkspaceAppearanceApi, defaultInviteSeatType, flagsFromSeatType, formatAppError, hasAppAccess, isErrorCode, normalizeError, normalizeThrown, planSeatSummary, resolveSubscriptionTier, seatTypeDescription, seatTypeFromFlags, seatTypeLabel, selectedToWebhookActions, sortedSuiteAiEvents, suiteAiEventSummary, tierAllowsTidesOnlySeats, tierAllowsTurtletime, tierLabel, useIsNight, webhookActionsToSelected };
419
+ export { type Api, type ApiClient, type ApiClientConfig, type ApiFetchInit, type AppError, type AuthReturnPath, type AuthReturnPathConfig, type BrandSettings, type BrandSettingsConfig, type CrossAppSync, type CrossAppSyncConfig, type CrossAppSyncTopic, DEFAULT_ERROR_CODE, ERROR_CODES, ERROR_COPY, ERROR_RETRYABLE, type ErrorCode, FREE_PLAN_SEAT_HELP, type JsonLdConfig, type LiveRefresh, type LiveRefreshOptions, type NotificationsClient, type NotificationsClientConfig, ORGANISATION_SEAT_HELP, type PortalHandoffPayload, type PortalHandoffRead, type SeatType, type SubscriptionTier, type SuiteAiJob, type SuiteAiJobEvent, type SuiteNotification, type SuiteNotificationsResponse, TIDES_ACCESS_HELP, TURTLETIME_ACCESS_HELP, WEBHOOK_ACTIVITY_ACTIONS, WORKSPACE_SEAT_HELP, type WebPushClient, type WebPushClientConfig, type WebPushConfig, type WebPushState, type WorkspaceAppearance, type WorkspaceAppearanceApiConfig, type WorkspaceAppearanceConfig, WorkspaceAppearanceError, type WorkspaceAppearanceHelpers, type WorkspaceAppearancePatch, type WorkspaceBrandPayload, type WorkspaceSubscription, type WorkspaceTheme, activeSuiteAiJobs, appErrorFromResponse, canDisableAppAccess, codeForStatus, copyFor, createApiClient, createAuthReturnPath, createBrandSettings, createCrossAppSync, createJsonLd, createLiveRefresh, createNotificationsClient, createWebPushClient, createWorkspaceAppearance, createWorkspaceAppearanceApi, defaultInviteSeatType, flagsFromSeatType, formatAppError, hasAppAccess, isErrorCode, normalizeError, normalizeThrown, planSeatSummary, resolveSubscriptionTier, seatTypeDescription, seatTypeFromFlags, seatTypeLabel, selectedToWebhookActions, sortedSuiteAiEvents, suiteAiEventSummary, tierAllowsTidesOnlySeats, tierAllowsTurtletime, tierLabel, useIsNight, webhookActionsToSelected };
package/dist/index.d.ts CHANGED
@@ -45,6 +45,9 @@ declare function normalizeThrown(input: unknown, capability?: string): AppError;
45
45
  /** Display string for a failure surface: capability copy plus the code. */
46
46
  declare function formatAppError(error: AppError): string;
47
47
 
48
+ type ApiFetchInit = RequestInit & {
49
+ skipAuth?: boolean;
50
+ };
48
51
  interface ApiClientConfig {
49
52
  /** localStorage key namespace, e.g. `tt` or `tides`. */
50
53
  storagePrefix: string;
@@ -61,10 +64,17 @@ interface ApiClientConfig {
61
64
  workspaceCookieDomain?: string;
62
65
  /** Fired when the active workspace changes in this tab. */
63
66
  onWorkspaceChange?: (id: string | null) => void;
67
+ /** Fired during sign-out before storage is cleared. */
68
+ onSignOut?: () => void;
69
+ /** Clear the workspace key on sign-out (default `true`). */
70
+ clearWorkspaceOnSignOut?: boolean;
71
+ /** Extra localStorage keys removed on sign-out (e.g. portal app tokens). */
72
+ extraStorageKeysOnClear?: string[];
64
73
  /** sessionStorage key prefixes to clear on sign-out. */
65
74
  sessionStorageClearPrefixes?: string[];
75
+ /** Refuse to send the session to an unexpected absolute origin. */
76
+ enforceSameOrigin?: boolean;
66
77
  }
67
- type ApiFetch = (path: string, options?: RequestInit) => Promise<Response>;
68
78
  /**
69
79
  * Build a browser API client scoped to one ShellStack app. Every app shares the
70
80
  * same fetch/refresh/workspace behaviour; only the storage namespace and
@@ -73,9 +83,10 @@ type ApiFetch = (path: string, options?: RequestInit) => Promise<Response>;
73
83
  declare function createApiClient(config: ApiClientConfig): {
74
84
  API_URL: string;
75
85
  onAuthFailure: (handler: () => void) => void;
76
- setTokens: (access: string, refresh: string) => void;
86
+ setTokens: (access: string, refresh?: string | null) => void;
77
87
  getAccessToken: () => string | null;
78
88
  getRefreshToken: () => string | null;
89
+ hasAccessToken: () => boolean;
79
90
  setActiveWorkspaceId: (id: string | null, options?: {
80
91
  share?: boolean;
81
92
  }) => void;
@@ -86,12 +97,14 @@ declare function createApiClient(config: ApiClientConfig): {
86
97
  refreshAccessToken: () => Promise<boolean>;
87
98
  parseApiError: (res: Response) => Promise<string>;
88
99
  api: {
89
- get: (path: string) => Promise<Response>;
90
- post: (path: string, body?: unknown, options?: RequestInit) => Promise<Response>;
91
- put: (path: string, body?: unknown) => Promise<Response>;
92
- patch: (path: string, body?: unknown) => Promise<Response>;
93
- delete: (path: string) => Promise<Response>;
100
+ get: (path: string, init?: ApiFetchInit) => Promise<Response>;
101
+ post: (path: string, body?: unknown, init?: ApiFetchInit) => Promise<Response>;
102
+ put: (path: string, body?: unknown, init?: ApiFetchInit) => Promise<Response>;
103
+ patch: (path: string, body?: unknown, init?: ApiFetchInit) => Promise<Response>;
104
+ delete: (path: string, init?: ApiFetchInit) => Promise<Response>;
94
105
  };
106
+ restoreSession: () => void;
107
+ clearSession: () => void;
95
108
  };
96
109
  type ApiClient = ReturnType<typeof createApiClient>;
97
110
  type Api = ApiClient['api'];
@@ -313,6 +326,36 @@ declare function createNotificationsClient(config: NotificationsClientConfig): {
313
326
  markAllSuiteNotificationsRead: () => Promise<void>;
314
327
  };
315
328
 
329
+ /** Browser push state surfaced to settings UI. */
330
+ type WebPushState = 'unsupported' | 'unconfigured' | 'default' | 'granted' | 'denied' | 'subscribed';
331
+ interface WebPushConfig {
332
+ enabled: boolean;
333
+ public_key: string;
334
+ }
335
+ interface WebPushClientConfig {
336
+ api: Api;
337
+ parseApiError: (res: Response) => Promise<string>;
338
+ publicKeyPath?: string;
339
+ subscribePath?: string;
340
+ unsubscribePath?: string;
341
+ serviceWorkerPath?: string;
342
+ serviceWorkerScope?: string;
343
+ }
344
+ type WebPushClient = ReturnType<typeof createWebPushClient>;
345
+ /**
346
+ * Browser Web Push client shared across ShellStack apps.
347
+ *
348
+ * Registration is per origin: each app ships its own `/sw.js` and manifest,
349
+ * but all talk to the same suite endpoints, so a subscription made in one app
350
+ * receives notifications for the whole suite.
351
+ */
352
+ declare function createWebPushClient(config: WebPushClientConfig): {
353
+ supported: () => boolean;
354
+ getState: () => Promise<WebPushState>;
355
+ enable: () => Promise<void>;
356
+ disable: () => Promise<void>;
357
+ };
358
+
316
359
  type SuiteAiJobEvent = {
317
360
  sequence: number;
318
361
  event_type: 'queued' | 'started' | 'progress' | 'tool' | 'completed' | 'failed' | 'cancelled';
@@ -373,4 +416,4 @@ declare function createJsonLd(config: JsonLdConfig): string;
373
416
 
374
417
  declare function useIsNight(): boolean;
375
418
 
376
- export { type Api, type ApiClient, type ApiClientConfig, type ApiFetch, type AppError, type AuthReturnPath, type AuthReturnPathConfig, type BrandSettings, type BrandSettingsConfig, type CrossAppSync, type CrossAppSyncConfig, type CrossAppSyncTopic, DEFAULT_ERROR_CODE, ERROR_CODES, ERROR_COPY, ERROR_RETRYABLE, type ErrorCode, FREE_PLAN_SEAT_HELP, type JsonLdConfig, type LiveRefresh, type LiveRefreshOptions, type NotificationsClient, type NotificationsClientConfig, ORGANISATION_SEAT_HELP, type PortalHandoffPayload, type PortalHandoffRead, type SeatType, type SubscriptionTier, type SuiteAiJob, type SuiteAiJobEvent, type SuiteNotification, type SuiteNotificationsResponse, TIDES_ACCESS_HELP, TURTLETIME_ACCESS_HELP, WEBHOOK_ACTIVITY_ACTIONS, WORKSPACE_SEAT_HELP, type WorkspaceAppearance, type WorkspaceAppearanceApiConfig, type WorkspaceAppearanceConfig, WorkspaceAppearanceError, type WorkspaceAppearanceHelpers, type WorkspaceAppearancePatch, type WorkspaceBrandPayload, type WorkspaceSubscription, type WorkspaceTheme, activeSuiteAiJobs, appErrorFromResponse, canDisableAppAccess, codeForStatus, copyFor, createApiClient, createAuthReturnPath, createBrandSettings, createCrossAppSync, createJsonLd, createLiveRefresh, createNotificationsClient, createWorkspaceAppearance, createWorkspaceAppearanceApi, defaultInviteSeatType, flagsFromSeatType, formatAppError, hasAppAccess, isErrorCode, normalizeError, normalizeThrown, planSeatSummary, resolveSubscriptionTier, seatTypeDescription, seatTypeFromFlags, seatTypeLabel, selectedToWebhookActions, sortedSuiteAiEvents, suiteAiEventSummary, tierAllowsTidesOnlySeats, tierAllowsTurtletime, tierLabel, useIsNight, webhookActionsToSelected };
419
+ export { type Api, type ApiClient, type ApiClientConfig, type ApiFetchInit, type AppError, type AuthReturnPath, type AuthReturnPathConfig, type BrandSettings, type BrandSettingsConfig, type CrossAppSync, type CrossAppSyncConfig, type CrossAppSyncTopic, DEFAULT_ERROR_CODE, ERROR_CODES, ERROR_COPY, ERROR_RETRYABLE, type ErrorCode, FREE_PLAN_SEAT_HELP, type JsonLdConfig, type LiveRefresh, type LiveRefreshOptions, type NotificationsClient, type NotificationsClientConfig, ORGANISATION_SEAT_HELP, type PortalHandoffPayload, type PortalHandoffRead, type SeatType, type SubscriptionTier, type SuiteAiJob, type SuiteAiJobEvent, type SuiteNotification, type SuiteNotificationsResponse, TIDES_ACCESS_HELP, TURTLETIME_ACCESS_HELP, WEBHOOK_ACTIVITY_ACTIONS, WORKSPACE_SEAT_HELP, type WebPushClient, type WebPushClientConfig, type WebPushConfig, type WebPushState, type WorkspaceAppearance, type WorkspaceAppearanceApiConfig, type WorkspaceAppearanceConfig, WorkspaceAppearanceError, type WorkspaceAppearanceHelpers, type WorkspaceAppearancePatch, type WorkspaceBrandPayload, type WorkspaceSubscription, type WorkspaceTheme, activeSuiteAiJobs, appErrorFromResponse, canDisableAppAccess, codeForStatus, copyFor, createApiClient, createAuthReturnPath, createBrandSettings, createCrossAppSync, createJsonLd, createLiveRefresh, createNotificationsClient, createWebPushClient, createWorkspaceAppearance, createWorkspaceAppearanceApi, defaultInviteSeatType, flagsFromSeatType, formatAppError, hasAppAccess, isErrorCode, normalizeError, normalizeThrown, planSeatSummary, resolveSubscriptionTier, seatTypeDescription, seatTypeFromFlags, seatTypeLabel, selectedToWebhookActions, sortedSuiteAiEvents, suiteAiEventSummary, tierAllowsTidesOnlySeats, tierAllowsTurtletime, tierLabel, useIsNight, webhookActionsToSelected };
package/dist/index.js CHANGED
@@ -43,6 +43,7 @@ __export(index_exports, {
43
43
  createJsonLd: () => createJsonLd,
44
44
  createLiveRefresh: () => createLiveRefresh,
45
45
  createNotificationsClient: () => createNotificationsClient,
46
+ createWebPushClient: () => createWebPushClient,
46
47
  createWorkspaceAppearance: () => createWorkspaceAppearance,
47
48
  createWorkspaceAppearanceApi: () => createWorkspaceAppearanceApi,
48
49
  defaultInviteSeatType: () => defaultInviteSeatType,
@@ -211,6 +212,7 @@ function createApiClient(config) {
211
212
  const ACCESS_KEY = config.accessTokenKey ?? `${config.storagePrefix}_access_token`;
212
213
  const REFRESH_KEY = config.refreshTokenKey ?? `${config.storagePrefix}_refresh_token`;
213
214
  const WORKSPACE_KEY = config.workspaceKey ?? `${config.storagePrefix}_active_workspace_id`;
215
+ const CLEAR_WORKSPACE = config.clearWorkspaceOnSignOut ?? true;
214
216
  let accessToken = null;
215
217
  let refreshToken = null;
216
218
  let activeWorkspaceId = null;
@@ -248,10 +250,12 @@ function createApiClient(config) {
248
250
  }
249
251
  function setTokens(access, refresh) {
250
252
  accessToken = access;
251
- refreshToken = refresh;
253
+ authFailureFired = false;
254
+ if (refresh !== void 0) refreshToken = refresh;
252
255
  if (typeof window !== "undefined") {
253
256
  window.localStorage.setItem(ACCESS_KEY, access);
254
- window.localStorage.setItem(REFRESH_KEY, refresh);
257
+ if (refresh) window.localStorage.setItem(REFRESH_KEY, refresh);
258
+ else if (refresh !== void 0) window.localStorage.removeItem(REFRESH_KEY);
255
259
  }
256
260
  }
257
261
  function getAccessToken() {
@@ -268,6 +272,9 @@ function createApiClient(config) {
268
272
  }
269
273
  return refreshToken;
270
274
  }
275
+ function hasAccessToken() {
276
+ return Boolean(getAccessToken());
277
+ }
271
278
  function setActiveWorkspaceId(id, options) {
272
279
  const changed = activeWorkspaceId !== id;
273
280
  activeWorkspaceId = id;
@@ -294,13 +301,17 @@ function createApiClient(config) {
294
301
  }
295
302
  }
296
303
  function clearAuth() {
304
+ config.onSignOut?.();
297
305
  accessToken = null;
298
306
  refreshToken = null;
299
307
  activeWorkspaceId = null;
300
308
  if (typeof window !== "undefined") {
301
309
  window.localStorage.removeItem(ACCESS_KEY);
302
310
  window.localStorage.removeItem(REFRESH_KEY);
303
- window.localStorage.removeItem(WORKSPACE_KEY);
311
+ if (CLEAR_WORKSPACE) window.localStorage.removeItem(WORKSPACE_KEY);
312
+ for (const key of config.extraStorageKeysOnClear ?? []) {
313
+ window.localStorage.removeItem(key);
314
+ }
304
315
  const prefixes = config.sessionStorageClearPrefixes ?? [];
305
316
  if (prefixes.length > 0) {
306
317
  try {
@@ -359,36 +370,43 @@ function createApiClient(config) {
359
370
  return false;
360
371
  }
361
372
  }
362
- async function apiFetch(path, options = {}) {
373
+ async function apiFetch(path, init = {}) {
374
+ const { skipAuth, ...requestInit } = init;
363
375
  const token = getAccessToken();
364
376
  const headers = {
365
377
  Accept: "application/json",
366
- ...options.headers
378
+ ...requestInit.headers
367
379
  };
368
- const body = options.body;
380
+ const body = requestInit.body;
369
381
  const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
370
382
  if (!isFormData) {
371
383
  headers["Content-Type"] = "application/json";
372
384
  }
373
- if (token) {
385
+ if (token && !skipAuth) {
374
386
  headers["Authorization"] = `Bearer ${token}`;
375
387
  }
376
388
  const wsId = getActiveWorkspaceId();
377
- if (wsId) {
389
+ if (wsId && !skipAuth) {
378
390
  headers["X-Active-Workspace-Id"] = wsId;
379
391
  }
380
392
  const url = path.startsWith("http") ? path : `${API_URL}${path}`;
393
+ if (config.enforceSameOrigin && !skipAuth && path.startsWith("http") && !url.startsWith(API_URL)) {
394
+ throw new Error("Refusing to send the session to an unexpected origin.");
395
+ }
381
396
  async function doFetch() {
382
397
  try {
383
- return await fetch(url, { ...options, headers });
384
- } catch {
398
+ return await fetch(url, { ...requestInit, headers });
399
+ } catch (error) {
400
+ if (init.signal?.aborted || error instanceof DOMException && error.name === "AbortError") {
401
+ throw error;
402
+ }
385
403
  throw new Error(
386
404
  `Could not reach the API at ${API_URL}. Is the Django backend running?`
387
405
  );
388
406
  }
389
407
  }
390
408
  let res = await doFetch();
391
- if (res.status === 401 && token) {
409
+ if (res.status === 401 && token && !skipAuth) {
392
410
  const refreshed = await refreshAccessToken();
393
411
  if (refreshed) {
394
412
  const newToken = getAccessToken();
@@ -396,9 +414,10 @@ function createApiClient(config) {
396
414
  headers["Authorization"] = `Bearer ${newToken}`;
397
415
  }
398
416
  res = await doFetch();
399
- if (res.status === 401) {
400
- triggerAuthFailure();
401
- }
417
+ }
418
+ if (res.status === 401) {
419
+ clearAuth();
420
+ triggerAuthFailure();
402
421
  }
403
422
  }
404
423
  return res;
@@ -407,21 +426,23 @@ function createApiClient(config) {
407
426
  return formatAppError(await appErrorFromResponse(res));
408
427
  }
409
428
  const api = {
410
- get: (path) => apiFetch(path),
411
- post: (path, body, options = {}) => apiFetch(path, {
412
- ...options,
429
+ get: (path, init) => apiFetch(path, init),
430
+ post: (path, body, init = {}) => apiFetch(path, {
431
+ ...init,
413
432
  method: "POST",
414
433
  body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0
415
434
  }),
416
- put: (path, body) => apiFetch(path, {
435
+ put: (path, body, init = {}) => apiFetch(path, {
436
+ ...init,
417
437
  method: "PUT",
418
438
  body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0
419
439
  }),
420
- patch: (path, body) => apiFetch(path, {
440
+ patch: (path, body, init = {}) => apiFetch(path, {
441
+ ...init,
421
442
  method: "PATCH",
422
443
  body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0
423
444
  }),
424
- delete: (path) => apiFetch(path, { method: "DELETE" })
445
+ delete: (path, init = {}) => apiFetch(path, { ...init, method: "DELETE" })
425
446
  };
426
447
  return {
427
448
  API_URL,
@@ -429,6 +450,7 @@ function createApiClient(config) {
429
450
  setTokens,
430
451
  getAccessToken,
431
452
  getRefreshToken,
453
+ hasAccessToken,
432
454
  setActiveWorkspaceId,
433
455
  getActiveWorkspaceId,
434
456
  restoreWorkspace,
@@ -436,7 +458,10 @@ function createApiClient(config) {
436
458
  restoreTokens,
437
459
  refreshAccessToken,
438
460
  parseApiError,
439
- api
461
+ api,
462
+ // Aliases used by Kraken/Shelly.
463
+ restoreSession: restoreTokens,
464
+ clearSession: clearAuth
440
465
  };
441
466
  }
442
467
 
@@ -1028,6 +1053,105 @@ function createNotificationsClient(config) {
1028
1053
  };
1029
1054
  }
1030
1055
 
1056
+ // src/notifications/create-web-push-client.ts
1057
+ function createWebPushClient(config) {
1058
+ const { api, parseApiError } = config;
1059
+ const PUBLIC_KEY_PATH = config.publicKeyPath ?? "/api/notifications/push/public-key/";
1060
+ const SUBSCRIBE_PATH = config.subscribePath ?? "/api/notifications/push/subscribe/";
1061
+ const UNSUBSCRIBE_PATH = config.unsubscribePath ?? "/api/notifications/push/unsubscribe/";
1062
+ const SW_PATH = config.serviceWorkerPath ?? "/sw.js";
1063
+ const SW_SCOPE = config.serviceWorkerScope ?? "/";
1064
+ function supported() {
1065
+ return typeof window !== "undefined" && "serviceWorker" in navigator && "PushManager" in window && "Notification" in window;
1066
+ }
1067
+ async function fetchConfig() {
1068
+ const res = await api.get(PUBLIC_KEY_PATH);
1069
+ if (!res.ok) throw new Error(await parseApiError(res));
1070
+ return await res.json();
1071
+ }
1072
+ async function getRegistration() {
1073
+ const existing = await navigator.serviceWorker.getRegistration(SW_SCOPE);
1074
+ return existing ?? null;
1075
+ }
1076
+ function urlBase64ToUint8Array(base64) {
1077
+ const padding = "=".repeat((4 - base64.length % 4) % 4);
1078
+ const normalized = (base64 + padding).replace(/-/g, "+").replace(/_/g, "/");
1079
+ const raw = atob(normalized);
1080
+ const output = new Uint8Array(raw.length);
1081
+ for (let i = 0; i < raw.length; i += 1) {
1082
+ output[i] = raw.charCodeAt(i);
1083
+ }
1084
+ return output;
1085
+ }
1086
+ async function getState() {
1087
+ if (!supported()) return "unsupported";
1088
+ let cfg;
1089
+ try {
1090
+ cfg = await fetchConfig();
1091
+ } catch {
1092
+ return "unconfigured";
1093
+ }
1094
+ if (!cfg.enabled || !cfg.public_key) return "unconfigured";
1095
+ if (Notification.permission === "denied") return "denied";
1096
+ const registration = await getRegistration();
1097
+ const subscription = await registration?.pushManager.getSubscription();
1098
+ if (subscription && Notification.permission === "granted") {
1099
+ return "subscribed";
1100
+ }
1101
+ if (Notification.permission === "granted") return "granted";
1102
+ return "default";
1103
+ }
1104
+ async function enable() {
1105
+ if (!supported()) {
1106
+ throw new Error("This browser does not support notifications.");
1107
+ }
1108
+ const permission = await Notification.requestPermission();
1109
+ if (permission !== "granted") {
1110
+ throw new Error("Notification permission was not granted.");
1111
+ }
1112
+ const cfg = await fetchConfig();
1113
+ if (!cfg.enabled || !cfg.public_key) {
1114
+ throw new Error("Push notifications are not configured on the server.");
1115
+ }
1116
+ let registration = await getRegistration();
1117
+ if (!registration) {
1118
+ registration = await navigator.serviceWorker.register(SW_PATH, {
1119
+ scope: SW_SCOPE
1120
+ });
1121
+ }
1122
+ await navigator.serviceWorker.ready;
1123
+ const existing = await registration.pushManager.getSubscription();
1124
+ const subscription = existing ?? await registration.pushManager.subscribe({
1125
+ userVisibleOnly: true,
1126
+ applicationServerKey: urlBase64ToUint8Array(cfg.public_key)
1127
+ });
1128
+ const json = subscription.toJSON();
1129
+ if (!json.endpoint || !json.keys?.p256dh || !json.keys?.auth) {
1130
+ throw new Error("The browser returned an incomplete push subscription.");
1131
+ }
1132
+ const res = await api.post(SUBSCRIBE_PATH, {
1133
+ endpoint: json.endpoint,
1134
+ keys: { p256dh: json.keys.p256dh, auth: json.keys.auth },
1135
+ user_agent: typeof navigator !== "undefined" ? navigator.userAgent : ""
1136
+ });
1137
+ if (!res.ok) throw new Error(await parseApiError(res));
1138
+ }
1139
+ async function disable() {
1140
+ if (!supported()) return;
1141
+ const registration = await getRegistration();
1142
+ const subscription = await registration?.pushManager.getSubscription();
1143
+ if (!subscription) return;
1144
+ const endpoint = subscription.endpoint;
1145
+ try {
1146
+ await subscription.unsubscribe();
1147
+ } catch {
1148
+ }
1149
+ const res = await api.post(UNSUBSCRIBE_PATH, { endpoint });
1150
+ if (!res.ok) throw new Error(await parseApiError(res));
1151
+ }
1152
+ return { supported, getState, enable, disable };
1153
+ }
1154
+
1031
1155
  // src/ai/suite-ai-job.ts
1032
1156
  function activeSuiteAiJobs(jobs) {
1033
1157
  return jobs.filter(
@@ -1156,6 +1280,7 @@ function useIsNight() {
1156
1280
  createJsonLd,
1157
1281
  createLiveRefresh,
1158
1282
  createNotificationsClient,
1283
+ createWebPushClient,
1159
1284
  createWorkspaceAppearance,
1160
1285
  createWorkspaceAppearanceApi,
1161
1286
  defaultInviteSeatType,