@xenide-io/suite-client 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Xenide
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # @xenide-io/suite-client
2
+
3
+ Framework-agnostic platform layer shared across the Xenide ShellStack apps
4
+ (TurtleTime, Tides, ShellStack, Kraken, Shelly). The UI counterpart lives in
5
+ [`@xenide-io/the-old-ui-theme`](https://www.npmjs.com/package/@xenide-io/the-old-ui-theme);
6
+ this package deliberately contains **no visual components**.
7
+
8
+ ## What's here
9
+
10
+ | Area | Exports |
11
+ | --- | --- |
12
+ | Errors | `ERROR_CODES`, `normalizeError`, `appErrorFromResponse`, `formatAppError` |
13
+ | API client | `createApiClient(config)` — fetch, JWT refresh, `X-Active-Workspace-Id`, shared workspace cookie |
14
+ | Auth | `createAuthReturnPath(config)` — return paths, portal handoff, redeem |
15
+ | Workspace | `createBrandSettings`, `createWorkspaceAppearance`, `createWorkspaceAppearanceApi`, seat/subscription helpers |
16
+ | Cross-app sync | `createCrossAppSync(config)`, `createLiveRefresh(sync)` |
17
+ | Notifications | `createNotificationsClient({ api, parseApiError })` |
18
+ | AI jobs | `activeSuiteAiJobs`, `sortedSuiteAiEvents`, `suiteAiEventSummary` |
19
+ | Webhooks | `WEBHOOK_ACTIVITY_ACTIONS`, `webhookActionsToSelected`, `selectedToWebhookActions` |
20
+ | SEO | `createJsonLd(config)` |
21
+ | Hooks | `useIsNight()` |
22
+
23
+ ## Usage
24
+
25
+ Each app owns a thin `lib/api-client.ts` that configures the shared factory, so
26
+ existing imports keep working:
27
+
28
+ ```ts
29
+ import { createApiClient } from '@xenide-io/suite-client';
30
+
31
+ export const {
32
+ api,
33
+ parseApiError,
34
+ getAccessToken,
35
+ setTokens,
36
+ // ...
37
+ } = createApiClient({
38
+ storagePrefix: 'tt',
39
+ refreshPath: '/api/turtletime/auth/refresh/',
40
+ });
41
+ ```
42
+
43
+ ## Scripts
44
+
45
+ ```bash
46
+ bun run typecheck
47
+ bun run lint
48
+ bun run test
49
+ bun run build
50
+ ```
51
+
52
+ ## Releasing
53
+
54
+ Publishing runs from GitHub Actions on a published GitHub Release using npm
55
+ trusted publishing (OIDC) — see `.github/workflows/release.yml`. Bump
56
+ `version`, tag `vX.Y.Z`, publish the release.
@@ -0,0 +1,376 @@
1
+ declare const ERROR_CODES: {
2
+ readonly AUTH_001: "E-AUTH-001";
3
+ readonly AUTH_002: "E-AUTH-002";
4
+ readonly AUTH_003: "E-AUTH-003";
5
+ readonly VALID_001: "E-VALID-001";
6
+ readonly DATA_001: "E-DATA-001";
7
+ readonly DATA_002: "E-DATA-002";
8
+ readonly LIMIT_001: "E-LIMIT-001";
9
+ readonly NET_001: "E-NET-001";
10
+ readonly EXT_001: "E-EXT-001";
11
+ readonly EXT_002: "E-EXT-002";
12
+ readonly SYS_001: "E-SYS-001";
13
+ readonly SYS_002: "E-SYS-002";
14
+ readonly CLIENT_001: "E-CLIENT-001";
15
+ readonly CLIENT_002: "E-CLIENT-002";
16
+ };
17
+ type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
18
+ declare const DEFAULT_ERROR_CODE: ErrorCode;
19
+ declare const ERROR_COPY: Record<ErrorCode, string>;
20
+ declare const ERROR_RETRYABLE: Record<ErrorCode, boolean>;
21
+ declare function isErrorCode(value: unknown): value is ErrorCode;
22
+ declare function copyFor(code: ErrorCode, capability?: string): string;
23
+
24
+ /**
25
+ * The single normalised shape every caught failure is reduced to before any
26
+ * component sees it. `message` is always resolved from the local registry;
27
+ * upstream text is never carried through.
28
+ */
29
+ type AppError = {
30
+ code: ErrorCode;
31
+ message: string;
32
+ correlationId?: string;
33
+ retryable: boolean;
34
+ };
35
+ declare function codeForStatus(status: number): ErrorCode;
36
+ /**
37
+ * Reduce an unknown value to an `AppError`. Accepts only the envelope shape;
38
+ * every other key on the input is discarded. Unknown or absent codes fall back.
39
+ */
40
+ declare function normalizeError(input: unknown, fallback?: ErrorCode, capability?: string): AppError;
41
+ /** Normalise a non-2xx `Response` without ever reading its raw message. */
42
+ declare function appErrorFromResponse(response: Response, capability?: string): Promise<AppError>;
43
+ /** Normalise a thrown fetch failure: offline, aborted, or unreachable. */
44
+ declare function normalizeThrown(input: unknown, capability?: string): AppError;
45
+ /** Display string for a failure surface: capability copy plus the code. */
46
+ declare function formatAppError(error: AppError): string;
47
+
48
+ interface ApiClientConfig {
49
+ /** localStorage key namespace, e.g. `tt` or `tides`. */
50
+ storagePrefix: string;
51
+ /** Backend refresh endpoint, e.g. `/api/turtletime/auth/refresh/`. */
52
+ refreshPath: string;
53
+ /** Overrides `NEXT_PUBLIC_API_URL` / `http://localhost:8000`. */
54
+ baseUrl?: string;
55
+ accessTokenKey?: string;
56
+ refreshTokenKey?: string;
57
+ workspaceKey?: string;
58
+ /** Suite-wide workspace cookie name. */
59
+ workspaceCookieName?: string;
60
+ /** Overrides `NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN`. */
61
+ workspaceCookieDomain?: string;
62
+ /** Fired when the active workspace changes in this tab. */
63
+ onWorkspaceChange?: (id: string | null) => void;
64
+ /** sessionStorage key prefixes to clear on sign-out. */
65
+ sessionStorageClearPrefixes?: string[];
66
+ }
67
+ type ApiFetch = (path: string, options?: RequestInit) => Promise<Response>;
68
+ /**
69
+ * Build a browser API client scoped to one ShellStack app. Every app shares the
70
+ * same fetch/refresh/workspace behaviour; only the storage namespace and
71
+ * refresh path differ.
72
+ */
73
+ declare function createApiClient(config: ApiClientConfig): {
74
+ API_URL: string;
75
+ onAuthFailure: (handler: () => void) => void;
76
+ setTokens: (access: string, refresh: string) => void;
77
+ getAccessToken: () => string | null;
78
+ getRefreshToken: () => string | null;
79
+ setActiveWorkspaceId: (id: string | null, options?: {
80
+ share?: boolean;
81
+ }) => void;
82
+ getActiveWorkspaceId: () => string | null;
83
+ restoreWorkspace: () => void;
84
+ clearAuth: () => void;
85
+ restoreTokens: () => void;
86
+ refreshAccessToken: () => Promise<boolean>;
87
+ parseApiError: (res: Response) => Promise<string>;
88
+ 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>;
94
+ };
95
+ };
96
+ type ApiClient = ReturnType<typeof createApiClient>;
97
+ type Api = ApiClient['api'];
98
+
99
+ interface AuthReturnPathConfig {
100
+ /** sessionStorage key holding the in-app return path, e.g. `tt_auth_return_to`. */
101
+ returnKey: string;
102
+ /** sessionStorage key caching the portal handoff payload. */
103
+ handoffKey?: string;
104
+ defaultNext?: string;
105
+ portalCallbackPath?: string;
106
+ redeemPath?: string;
107
+ }
108
+ interface PortalHandoffPayload {
109
+ code?: string;
110
+ token?: string;
111
+ access?: string;
112
+ refresh?: string;
113
+ }
114
+ interface PortalHandoffRead {
115
+ code: string | null;
116
+ token: string | null;
117
+ refresh: string | null;
118
+ next: string;
119
+ }
120
+ type AuthReturnPath = ReturnType<typeof createAuthReturnPath>;
121
+ declare function createAuthReturnPath(config: AuthReturnPathConfig): {
122
+ isSafeAuthReturnPath: (path: string | null | undefined) => path is string;
123
+ stashAuthReturnPath: (path: string | null | undefined) => void;
124
+ portalHandoffUrl: (base: string, data: PortalHandoffPayload, next?: string, fallbackNext?: string) => string;
125
+ readPortalHandoff: (fallbackNext?: string) => PortalHandoffRead;
126
+ redeemPortalHandoff: (apiUrl: string, fallbackNext?: string) => Promise<PortalHandoffRead>;
127
+ readAuthReturnPath: () => string | null;
128
+ signInHrefWithReturn: (fallbackRedirect?: string | null) => string;
129
+ registerHrefWithReturn: (returnPath: string) => string;
130
+ };
131
+
132
+ /** Copy and helpers for workspace seats vs per-app access (shared core model). */
133
+ declare const WORKSPACE_SEAT_HELP = "Each member uses one workspace seat. Inviting someone reserves a seat until the invite is revoked or accepted.";
134
+ declare const ORGANISATION_SEAT_HELP = "Each organisation member uses one seat across all your workspaces. One invite gives them access to every workspace in your organisation.";
135
+ declare const TIDES_ACCESS_HELP = "Tides \u2014 project management.";
136
+ declare const TURTLETIME_ACCESS_HELP = "TurtleTime \u2014 time tracking add-on (Basic plan and above).";
137
+ declare const FREE_PLAN_SEAT_HELP = "Free personal workspaces include one product app only. Create an organisation on ShellStack for both apps and integration.";
138
+ declare function hasAppAccess(canAccessTurtletime: boolean, canAccessTides: boolean): boolean;
139
+ declare function canDisableAppAccess(app: 'turtletime' | 'tides', canAccessTurtletime: boolean, canAccessTides: boolean): boolean;
140
+
141
+ /** Per-workspace subscription tier helpers (mirrors core/subscriptions.py). */
142
+ type SubscriptionTier = 'free' | 'plus' | 'ai_plus' | 'basic' | 'pro' | 'enterprise';
143
+ interface WorkspaceSubscription {
144
+ tier: SubscriptionTier;
145
+ tier_label: string;
146
+ limits: {
147
+ seats?: number | null;
148
+ integrated_seats: number | null;
149
+ tides_only_seats: number | null;
150
+ };
151
+ usage: {
152
+ seats?: number;
153
+ integrated_seats: number;
154
+ tides_only_seats: number;
155
+ };
156
+ }
157
+ type SeatType = 'tides' | 'integrated';
158
+ declare function tierLabel(tier: SubscriptionTier): string;
159
+ declare function resolveSubscriptionTier(workspace: {
160
+ subscription?: {
161
+ tier?: string;
162
+ } | null;
163
+ subscription_tier?: string | null;
164
+ } | null | undefined): SubscriptionTier;
165
+ declare function tierAllowsTurtletime(tier: SubscriptionTier): boolean;
166
+ declare function defaultInviteSeatType(tier: SubscriptionTier): SeatType;
167
+ declare function tierAllowsTidesOnlySeats(): boolean;
168
+ declare function seatTypeFromFlags(canAccessTurtletime: boolean, canAccessTides: boolean): SeatType;
169
+ declare function flagsFromSeatType(seatType: SeatType): {
170
+ can_access_turtletime: boolean;
171
+ can_access_tides: boolean;
172
+ };
173
+ declare function seatTypeLabel(seatType: SeatType): string;
174
+ declare function seatTypeDescription(tier: SubscriptionTier, seatType: SeatType): string;
175
+ declare function planSeatSummary(subscription: WorkspaceSubscription | null | undefined): string;
176
+
177
+ interface BrandSettingsConfig {
178
+ /** Fallback title when the workspace has not customised one. */
179
+ defaultTitle: string;
180
+ /** Window event dispatched when the brand changes. */
181
+ changedEvent: string;
182
+ }
183
+ interface WorkspaceBrandPayload {
184
+ brand_logo?: string | null;
185
+ brand_title?: string | null;
186
+ brand_show_title?: boolean;
187
+ }
188
+ type BrandSettings = ReturnType<typeof createBrandSettings>;
189
+ declare function createBrandSettings(config: BrandSettingsConfig): {
190
+ DEFAULT_BRAND_TITLE: string;
191
+ BRAND_SETTINGS_CHANGED: string;
192
+ /** @deprecated Use BRAND_SETTINGS_CHANGED */
193
+ BRAND_LOGO_CHANGED: string;
194
+ applyWorkspaceBrand: (payload: WorkspaceBrandPayload) => void;
195
+ getBrandLogo: () => string | null;
196
+ getCustomBrandTitle: () => string | null;
197
+ getBrandShowTitle: () => boolean;
198
+ getDisplayBrandTitle: () => string | null;
199
+ setBrandLogo: (dataUrl: string | null) => void;
200
+ setCustomBrandTitle: (title: string | null) => void;
201
+ setBrandShowTitle: (show: boolean) => void;
202
+ resetBrandTitle: () => void;
203
+ };
204
+
205
+ interface WorkspaceAppearanceConfig {
206
+ /** Window event dispatched when the active workspace changes. */
207
+ changedEvent: string;
208
+ /** localStorage base key for the theme. */
209
+ themeStorageBase: string;
210
+ /** Reads the active workspace id from the app's API client. */
211
+ getActiveWorkspaceId: () => string | null;
212
+ }
213
+ type WorkspaceAppearanceHelpers = ReturnType<typeof createWorkspaceAppearance>;
214
+ declare function createWorkspaceAppearance(config: WorkspaceAppearanceConfig): {
215
+ WORKSPACE_CHANGED: string;
216
+ THEME_STORAGE_BASE: string;
217
+ notifyWorkspaceChanged: () => void;
218
+ scopedStorageKey: (base: string, workspaceId?: string | null) => string;
219
+ getScopedStorageItem: (base: string, workspaceId?: string | null) => string | null;
220
+ setScopedStorageItem: (base: string, value: string | null, workspaceId?: string | null) => void;
221
+ };
222
+
223
+ interface WorkspaceAppearanceApiConfig {
224
+ api: Api;
225
+ parseApiError: (res: Response) => Promise<string>;
226
+ /** Endpoint base, e.g. `/api/turtletime/workspaces/current/appearance/`. */
227
+ basePath: string;
228
+ applyWorkspaceBrand: (payload: WorkspaceBrandPayload) => void;
229
+ }
230
+ type WorkspaceTheme = 'light' | 'dark';
231
+ interface WorkspaceAppearance {
232
+ theme: WorkspaceTheme;
233
+ brand_logo: string | null;
234
+ brand_title: string | null;
235
+ brand_show_title: boolean;
236
+ }
237
+ type WorkspaceAppearancePatch = Partial<Pick<WorkspaceAppearance, 'theme' | 'brand_title' | 'brand_show_title'>> & {
238
+ brand_logo_file?: File;
239
+ clear_brand_logo?: boolean;
240
+ };
241
+ declare class WorkspaceAppearanceError extends Error {
242
+ status: number;
243
+ constructor(message: string, status: number);
244
+ }
245
+ declare function createWorkspaceAppearanceApi(config: WorkspaceAppearanceApiConfig): {
246
+ WorkspaceAppearanceError: typeof WorkspaceAppearanceError;
247
+ fetchWorkspaceAppearance: () => Promise<WorkspaceAppearance>;
248
+ updateWorkspaceAppearance: (patch: WorkspaceAppearancePatch) => Promise<WorkspaceAppearance>;
249
+ cacheWorkspaceAppearanceLocally: (appearance: WorkspaceAppearance) => void;
250
+ };
251
+
252
+ type CrossAppSyncTopic = 'time' | 'integration' | 'tasks';
253
+ interface CrossAppSyncConfig {
254
+ api: Api;
255
+ /** Checkpoint endpoint, e.g. `/api/turtletime/sync-checkpoint/`. */
256
+ checkpointPath: string;
257
+ /** Poll interval while the tab is visible (ms). */
258
+ pollMs?: number;
259
+ }
260
+ type CrossAppSync = ReturnType<typeof createCrossAppSync>;
261
+ /**
262
+ * Poll backend checkpoints so sibling apps refresh when data mutates
263
+ * elsewhere. Same-tab instant updates stay the app's responsibility via its
264
+ * own window events.
265
+ */
266
+ declare function createCrossAppSync(config: CrossAppSyncConfig): {
267
+ broadcastCrossAppSync: (_topic: CrossAppSyncTopic) => void;
268
+ subscribeCrossAppSync: (onSync: () => void, topic?: CrossAppSyncTopic) => () => void;
269
+ lastCrossAppSyncTime: (topic?: CrossAppSyncTopic) => number;
270
+ };
271
+
272
+ interface LiveRefreshOptions {
273
+ /** Same-tab custom events (e.g. `tt-timer-mutated`). */
274
+ localEvents?: string[];
275
+ /** Gentle poll while the tab is visible (ms). */
276
+ pollMs?: number;
277
+ /** Limit cross-tab refreshes to one mutation topic. */
278
+ crossAppTopic?: CrossAppSyncTopic;
279
+ }
280
+ type LiveRefresh = ReturnType<typeof createLiveRefresh>;
281
+ declare function createLiveRefresh(sync: CrossAppSync): {
282
+ subscribeLiveRefresh: (refresh: () => void, options?: LiveRefreshOptions) => () => void;
283
+ hasPendingCrossAppSync: (topic: CrossAppSyncTopic | undefined, since: number) => boolean;
284
+ };
285
+
286
+ /** Cross-app (suite) notification from `/api/notifications/` — shared across ShellStack apps. */
287
+ interface SuiteNotification {
288
+ id: string;
289
+ title: string;
290
+ body: string;
291
+ href: string;
292
+ kind: string;
293
+ source_app: string;
294
+ read_at: string | null;
295
+ created_at: string;
296
+ workspace: string | null;
297
+ organisation: string | null;
298
+ }
299
+ interface SuiteNotificationsResponse {
300
+ notifications: SuiteNotification[];
301
+ unread_count: number;
302
+ }
303
+ interface NotificationsClientConfig {
304
+ api: Api;
305
+ parseApiError: (res: Response) => Promise<string>;
306
+ cacheMs?: number;
307
+ listPath?: string;
308
+ }
309
+ type NotificationsClient = ReturnType<typeof createNotificationsClient>;
310
+ declare function createNotificationsClient(config: NotificationsClientConfig): {
311
+ fetchSuiteNotifications: () => Promise<SuiteNotificationsResponse>;
312
+ markSuiteNotificationRead: (id: string) => Promise<void>;
313
+ markAllSuiteNotificationsRead: () => Promise<void>;
314
+ };
315
+
316
+ type SuiteAiJobEvent = {
317
+ sequence: number;
318
+ event_type: 'queued' | 'started' | 'progress' | 'tool' | 'completed' | 'failed' | 'cancelled';
319
+ summary: string;
320
+ payload: Record<string, unknown>;
321
+ created_at: string;
322
+ };
323
+ type SuiteAiJob = {
324
+ id: string;
325
+ status: 'pending' | 'running' | 'completed' | 'failed';
326
+ events: SuiteAiJobEvent[];
327
+ };
328
+ declare function activeSuiteAiJobs(jobs: SuiteAiJob[]): SuiteAiJob[];
329
+ declare function sortedSuiteAiEvents(events: SuiteAiJobEvent[]): SuiteAiJobEvent[];
330
+ declare function suiteAiEventSummary(event?: SuiteAiJobEvent): string;
331
+
332
+ declare const WEBHOOK_ACTIVITY_ACTIONS: readonly [{
333
+ readonly id: "created";
334
+ readonly label: "Task created";
335
+ }, {
336
+ readonly id: "updated";
337
+ readonly label: "Task updated";
338
+ }, {
339
+ readonly id: "status_changed";
340
+ readonly label: "Task status changed";
341
+ }, {
342
+ readonly id: "assigned";
343
+ readonly label: "Task assigned";
344
+ }, {
345
+ readonly id: "commented";
346
+ readonly label: "Comment posted";
347
+ }, {
348
+ readonly id: "project_renamed";
349
+ readonly label: "Project renamed";
350
+ }, {
351
+ readonly id: "project_status_changed";
352
+ readonly label: "Project archived or restored";
353
+ }];
354
+ declare function webhookActionsToSelected(stored: string[] | null | undefined): Set<string>;
355
+ declare function selectedToWebhookActions(selected: Set<string>): string[];
356
+
357
+ interface JsonLdConfig {
358
+ name: string;
359
+ url: string;
360
+ description: string;
361
+ applicationCategory?: string;
362
+ applicationSubCategory?: string;
363
+ operatingSystem?: string;
364
+ softwareVersion?: string;
365
+ price?: string;
366
+ priceCurrency?: string;
367
+ inLanguage?: string;
368
+ publisherName?: string;
369
+ publisherUrl?: string;
370
+ }
371
+ /** Build a schema.org SoftwareApplication JSON-LD string for a product page. */
372
+ declare function createJsonLd(config: JsonLdConfig): string;
373
+
374
+ declare function useIsNight(): boolean;
375
+
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 };