@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 +51 -8
- package/dist/index.d.ts +51 -8
- package/dist/index.js +146 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +145 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
...
|
|
378
|
+
...requestInit.headers
|
|
367
379
|
};
|
|
368
|
-
const 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, { ...
|
|
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
|
-
|
|
400
|
-
|
|
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,
|
|
412
|
-
...
|
|
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,
|