@xenide-io/suite-client 0.2.0 → 0.3.2
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 +21 -8
- package/dist/index.d.ts +21 -8
- package/dist/index.js +45 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +45 -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'];
|
|
@@ -373,4 +386,4 @@ declare function createJsonLd(config: JsonLdConfig): string;
|
|
|
373
386
|
|
|
374
387
|
declare function useIsNight(): boolean;
|
|
375
388
|
|
|
376
|
-
export { type Api, type ApiClient, type ApiClientConfig, type
|
|
389
|
+
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 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 };
|
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'];
|
|
@@ -373,4 +386,4 @@ declare function createJsonLd(config: JsonLdConfig): string;
|
|
|
373
386
|
|
|
374
387
|
declare function useIsNight(): boolean;
|
|
375
388
|
|
|
376
|
-
export { type Api, type ApiClient, type ApiClientConfig, type
|
|
389
|
+
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 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 };
|
package/dist/index.js
CHANGED
|
@@ -211,6 +211,7 @@ function createApiClient(config) {
|
|
|
211
211
|
const ACCESS_KEY = config.accessTokenKey ?? `${config.storagePrefix}_access_token`;
|
|
212
212
|
const REFRESH_KEY = config.refreshTokenKey ?? `${config.storagePrefix}_refresh_token`;
|
|
213
213
|
const WORKSPACE_KEY = config.workspaceKey ?? `${config.storagePrefix}_active_workspace_id`;
|
|
214
|
+
const CLEAR_WORKSPACE = config.clearWorkspaceOnSignOut ?? true;
|
|
214
215
|
let accessToken = null;
|
|
215
216
|
let refreshToken = null;
|
|
216
217
|
let activeWorkspaceId = null;
|
|
@@ -248,10 +249,12 @@ function createApiClient(config) {
|
|
|
248
249
|
}
|
|
249
250
|
function setTokens(access, refresh) {
|
|
250
251
|
accessToken = access;
|
|
251
|
-
|
|
252
|
+
authFailureFired = false;
|
|
253
|
+
if (refresh !== void 0) refreshToken = refresh;
|
|
252
254
|
if (typeof window !== "undefined") {
|
|
253
255
|
window.localStorage.setItem(ACCESS_KEY, access);
|
|
254
|
-
window.localStorage.setItem(REFRESH_KEY, refresh);
|
|
256
|
+
if (refresh) window.localStorage.setItem(REFRESH_KEY, refresh);
|
|
257
|
+
else if (refresh !== void 0) window.localStorage.removeItem(REFRESH_KEY);
|
|
255
258
|
}
|
|
256
259
|
}
|
|
257
260
|
function getAccessToken() {
|
|
@@ -268,6 +271,9 @@ function createApiClient(config) {
|
|
|
268
271
|
}
|
|
269
272
|
return refreshToken;
|
|
270
273
|
}
|
|
274
|
+
function hasAccessToken() {
|
|
275
|
+
return Boolean(getAccessToken());
|
|
276
|
+
}
|
|
271
277
|
function setActiveWorkspaceId(id, options) {
|
|
272
278
|
const changed = activeWorkspaceId !== id;
|
|
273
279
|
activeWorkspaceId = id;
|
|
@@ -294,13 +300,17 @@ function createApiClient(config) {
|
|
|
294
300
|
}
|
|
295
301
|
}
|
|
296
302
|
function clearAuth() {
|
|
303
|
+
config.onSignOut?.();
|
|
297
304
|
accessToken = null;
|
|
298
305
|
refreshToken = null;
|
|
299
306
|
activeWorkspaceId = null;
|
|
300
307
|
if (typeof window !== "undefined") {
|
|
301
308
|
window.localStorage.removeItem(ACCESS_KEY);
|
|
302
309
|
window.localStorage.removeItem(REFRESH_KEY);
|
|
303
|
-
window.localStorage.removeItem(WORKSPACE_KEY);
|
|
310
|
+
if (CLEAR_WORKSPACE) window.localStorage.removeItem(WORKSPACE_KEY);
|
|
311
|
+
for (const key of config.extraStorageKeysOnClear ?? []) {
|
|
312
|
+
window.localStorage.removeItem(key);
|
|
313
|
+
}
|
|
304
314
|
const prefixes = config.sessionStorageClearPrefixes ?? [];
|
|
305
315
|
if (prefixes.length > 0) {
|
|
306
316
|
try {
|
|
@@ -359,36 +369,43 @@ function createApiClient(config) {
|
|
|
359
369
|
return false;
|
|
360
370
|
}
|
|
361
371
|
}
|
|
362
|
-
async function apiFetch(path,
|
|
372
|
+
async function apiFetch(path, init = {}) {
|
|
373
|
+
const { skipAuth, ...requestInit } = init;
|
|
363
374
|
const token = getAccessToken();
|
|
364
375
|
const headers = {
|
|
365
376
|
Accept: "application/json",
|
|
366
|
-
...
|
|
377
|
+
...requestInit.headers
|
|
367
378
|
};
|
|
368
|
-
const body =
|
|
379
|
+
const body = requestInit.body;
|
|
369
380
|
const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
|
|
370
381
|
if (!isFormData) {
|
|
371
382
|
headers["Content-Type"] = "application/json";
|
|
372
383
|
}
|
|
373
|
-
if (token) {
|
|
384
|
+
if (token && !skipAuth) {
|
|
374
385
|
headers["Authorization"] = `Bearer ${token}`;
|
|
375
386
|
}
|
|
376
387
|
const wsId = getActiveWorkspaceId();
|
|
377
|
-
if (wsId) {
|
|
388
|
+
if (wsId && !skipAuth) {
|
|
378
389
|
headers["X-Active-Workspace-Id"] = wsId;
|
|
379
390
|
}
|
|
380
391
|
const url = path.startsWith("http") ? path : `${API_URL}${path}`;
|
|
392
|
+
if (config.enforceSameOrigin && !skipAuth && path.startsWith("http") && !url.startsWith(API_URL)) {
|
|
393
|
+
throw new Error("Refusing to send the session to an unexpected origin.");
|
|
394
|
+
}
|
|
381
395
|
async function doFetch() {
|
|
382
396
|
try {
|
|
383
|
-
return await fetch(url, { ...
|
|
384
|
-
} catch {
|
|
397
|
+
return await fetch(url, { ...requestInit, headers });
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (init.signal?.aborted || error instanceof DOMException && error.name === "AbortError") {
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
385
402
|
throw new Error(
|
|
386
403
|
`Could not reach the API at ${API_URL}. Is the Django backend running?`
|
|
387
404
|
);
|
|
388
405
|
}
|
|
389
406
|
}
|
|
390
407
|
let res = await doFetch();
|
|
391
|
-
if (res.status === 401 && token) {
|
|
408
|
+
if (res.status === 401 && token && !skipAuth) {
|
|
392
409
|
const refreshed = await refreshAccessToken();
|
|
393
410
|
if (refreshed) {
|
|
394
411
|
const newToken = getAccessToken();
|
|
@@ -396,9 +413,10 @@ function createApiClient(config) {
|
|
|
396
413
|
headers["Authorization"] = `Bearer ${newToken}`;
|
|
397
414
|
}
|
|
398
415
|
res = await doFetch();
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
416
|
+
}
|
|
417
|
+
if (res.status === 401) {
|
|
418
|
+
clearAuth();
|
|
419
|
+
triggerAuthFailure();
|
|
402
420
|
}
|
|
403
421
|
}
|
|
404
422
|
return res;
|
|
@@ -407,21 +425,23 @@ function createApiClient(config) {
|
|
|
407
425
|
return formatAppError(await appErrorFromResponse(res));
|
|
408
426
|
}
|
|
409
427
|
const api = {
|
|
410
|
-
get: (path) => apiFetch(path),
|
|
411
|
-
post: (path, body,
|
|
412
|
-
...
|
|
428
|
+
get: (path, init) => apiFetch(path, init),
|
|
429
|
+
post: (path, body, init = {}) => apiFetch(path, {
|
|
430
|
+
...init,
|
|
413
431
|
method: "POST",
|
|
414
432
|
body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0
|
|
415
433
|
}),
|
|
416
|
-
put: (path, body) => apiFetch(path, {
|
|
434
|
+
put: (path, body, init = {}) => apiFetch(path, {
|
|
435
|
+
...init,
|
|
417
436
|
method: "PUT",
|
|
418
437
|
body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0
|
|
419
438
|
}),
|
|
420
|
-
patch: (path, body) => apiFetch(path, {
|
|
439
|
+
patch: (path, body, init = {}) => apiFetch(path, {
|
|
440
|
+
...init,
|
|
421
441
|
method: "PATCH",
|
|
422
442
|
body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0
|
|
423
443
|
}),
|
|
424
|
-
delete: (path) => apiFetch(path, { method: "DELETE" })
|
|
444
|
+
delete: (path, init = {}) => apiFetch(path, { ...init, method: "DELETE" })
|
|
425
445
|
};
|
|
426
446
|
return {
|
|
427
447
|
API_URL,
|
|
@@ -429,6 +449,7 @@ function createApiClient(config) {
|
|
|
429
449
|
setTokens,
|
|
430
450
|
getAccessToken,
|
|
431
451
|
getRefreshToken,
|
|
452
|
+
hasAccessToken,
|
|
432
453
|
setActiveWorkspaceId,
|
|
433
454
|
getActiveWorkspaceId,
|
|
434
455
|
restoreWorkspace,
|
|
@@ -436,7 +457,10 @@ function createApiClient(config) {
|
|
|
436
457
|
restoreTokens,
|
|
437
458
|
refreshAccessToken,
|
|
438
459
|
parseApiError,
|
|
439
|
-
api
|
|
460
|
+
api,
|
|
461
|
+
// Aliases used by Kraken/Shelly.
|
|
462
|
+
restoreSession: restoreTokens,
|
|
463
|
+
clearSession: clearAuth
|
|
440
464
|
};
|
|
441
465
|
}
|
|
442
466
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/errors/codes.ts","../src/errors/normalize.ts","../src/api/create-api-client.ts","../src/auth/create-auth-return-path.ts","../src/workspace/access.ts","../src/workspace/subscription.ts","../src/workspace/create-brand-settings.ts","../src/workspace/create-workspace-appearance.ts","../src/workspace/create-workspace-appearance-api.ts","../src/sync/create-cross-app-sync.ts","../src/sync/create-live-refresh.ts","../src/notifications/create-notifications-client.ts","../src/ai/suite-ai-job.ts","../src/webhooks/webhook-activity-actions.ts","../src/seo/create-json-ld.ts","../src/hooks/use-is-night.ts"],"sourcesContent":["export * from './errors';\nexport * from './api/create-api-client';\nexport * from './auth/create-auth-return-path';\nexport * from './workspace/access';\nexport * from './workspace/subscription';\nexport * from './workspace/create-brand-settings';\nexport * from './workspace/create-workspace-appearance';\nexport * from './workspace/create-workspace-appearance-api';\nexport * from './sync/create-cross-app-sync';\nexport * from './sync/create-live-refresh';\nexport * from './notifications/create-notifications-client';\nexport * from './ai/suite-ai-job';\nexport * from './webhooks/webhook-activity-actions';\nexport * from './seo/create-json-ld';\nexport * from './hooks/use-is-night';\n","// AUTO-GENERATED from ERROR_CODES.md by scripts/error-codes/generate.py. Do not edit.\n\nexport const ERROR_CODES = {\n AUTH_001: 'E-AUTH-001',\n AUTH_002: 'E-AUTH-002',\n AUTH_003: 'E-AUTH-003',\n VALID_001: 'E-VALID-001',\n DATA_001: 'E-DATA-001',\n DATA_002: 'E-DATA-002',\n LIMIT_001: 'E-LIMIT-001',\n NET_001: 'E-NET-001',\n EXT_001: 'E-EXT-001',\n EXT_002: 'E-EXT-002',\n SYS_001: 'E-SYS-001',\n SYS_002: 'E-SYS-002',\n CLIENT_001: 'E-CLIENT-001',\n CLIENT_002: 'E-CLIENT-002',\n} as const;\n\nexport type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];\n\nexport const DEFAULT_ERROR_CODE: ErrorCode = 'E-SYS-001';\n\nexport const ERROR_COPY: Record<ErrorCode, string> = {\n 'E-AUTH-001': 'Sign-in was not accepted. Check your details and try again.',\n 'E-AUTH-002': 'Your session has ended. Sign in again to continue.',\n 'E-AUTH-003': 'You do not have access to this. Ask an administrator for access.',\n 'E-VALID-001': 'Some details need correcting before this can be saved.',\n 'E-DATA-001': 'We could not find what you asked for. It may have been removed.',\n 'E-DATA-002': 'This conflicts with existing information. Review it and try again.',\n 'E-LIMIT-001': 'Too many requests right now. Wait a moment and try again.',\n 'E-NET-001': 'We could not reach the service. Try again in a moment.',\n 'E-EXT-001': 'The <capability> is unavailable right now. Try again shortly.',\n 'E-EXT-002': 'The <capability> could not complete that request. Try again, or adjust the input.',\n 'E-SYS-001': 'Something went wrong on our side. We have recorded it. Try again.',\n 'E-SYS-002': 'This feature is temporarily unavailable while we carry out maintenance.',\n 'E-CLIENT-001': 'You appear to be offline. Check your connection and try again.',\n 'E-CLIENT-002': 'We could not read the response. Reload the page and try again.',\n};\n\nexport const ERROR_RETRYABLE: Record<ErrorCode, boolean> = {\n 'E-AUTH-001': false,\n 'E-AUTH-002': true,\n 'E-AUTH-003': false,\n 'E-VALID-001': false,\n 'E-DATA-001': false,\n 'E-DATA-002': false,\n 'E-LIMIT-001': true,\n 'E-NET-001': true,\n 'E-EXT-001': true,\n 'E-EXT-002': true,\n 'E-SYS-001': true,\n 'E-SYS-002': false,\n 'E-CLIENT-001': true,\n 'E-CLIENT-002': true,\n};\n\nconst ERROR_CODE_SET = new Set<string>(Object.values(ERROR_CODES));\n\nexport function isErrorCode(value: unknown): value is ErrorCode {\n return typeof value === 'string' && ERROR_CODE_SET.has(value);\n}\n\nexport function copyFor(code: ErrorCode, capability?: string): string {\n const copy = ERROR_COPY[code] ?? ERROR_COPY[DEFAULT_ERROR_CODE];\n if (copy.includes('<capability>')) {\n return copy.replace('<capability>', capability ?? 'service');\n }\n return copy;\n}\n","import {\n DEFAULT_ERROR_CODE,\n ERROR_CODES,\n ERROR_RETRYABLE,\n copyFor,\n isErrorCode,\n type ErrorCode,\n} from './codes';\n\n/**\n * The single normalised shape every caught failure is reduced to before any\n * component sees it. `message` is always resolved from the local registry;\n * upstream text is never carried through.\n */\nexport type AppError = {\n code: ErrorCode;\n message: string;\n correlationId?: string;\n retryable: boolean;\n};\n\nconst STATUS_TO_CODE: Record<number, ErrorCode> = {\n 400: ERROR_CODES.VALID_001,\n 401: ERROR_CODES.AUTH_001,\n 403: ERROR_CODES.AUTH_003,\n 404: ERROR_CODES.DATA_001,\n 405: ERROR_CODES.SYS_001,\n 409: ERROR_CODES.DATA_002,\n 422: ERROR_CODES.VALID_001,\n 429: ERROR_CODES.LIMIT_001,\n 500: ERROR_CODES.SYS_001,\n 502: ERROR_CODES.EXT_002,\n 503: ERROR_CODES.NET_001,\n};\n\nexport function codeForStatus(status: number): ErrorCode {\n if (STATUS_TO_CODE[status]) return STATUS_TO_CODE[status];\n if (status >= 500) return ERROR_CODES.SYS_001;\n if (status >= 400) return ERROR_CODES.VALID_001;\n return ERROR_CODES.SYS_001;\n}\n\nfunction build(\n code: ErrorCode,\n correlationId?: string,\n retryable?: boolean,\n capability?: string,\n): AppError {\n return {\n code,\n message: copyFor(code, capability),\n ...(correlationId ? { correlationId } : {}),\n retryable: retryable ?? ERROR_RETRYABLE[code],\n };\n}\n\nfunction extractEnvelope(\n input: unknown,\n): { code: string; correlationId?: string; retryable?: boolean } | null {\n if (typeof input !== 'object' || input === null) return null;\n const error = (input as Record<string, unknown>).error;\n if (typeof error !== 'object' || error === null) return null;\n const record = error as Record<string, unknown>;\n return {\n code: typeof record.code === 'string' ? record.code : '',\n correlationId:\n typeof record.correlation_id === 'string'\n ? record.correlation_id\n : undefined,\n retryable:\n typeof record.retryable === 'boolean' ? record.retryable : undefined,\n };\n}\n\n/**\n * Reduce an unknown value to an `AppError`. Accepts only the envelope shape;\n * every other key on the input is discarded. Unknown or absent codes fall back.\n */\nexport function normalizeError(\n input: unknown,\n fallback: ErrorCode = DEFAULT_ERROR_CODE,\n capability?: string,\n): AppError {\n const envelope = extractEnvelope(input);\n if (!envelope) return build(fallback, undefined, undefined, capability);\n const resolved = isErrorCode(envelope.code) ? envelope.code : fallback;\n return build(\n resolved,\n envelope.correlationId,\n envelope.retryable,\n capability,\n );\n}\n\n/** Normalise a non-2xx `Response` without ever reading its raw message. */\nexport async function appErrorFromResponse(\n response: Response,\n capability?: string,\n): Promise<AppError> {\n const fallback = codeForStatus(response.status);\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = undefined;\n }\n return normalizeError(body, fallback, capability);\n}\n\n/** Normalise a thrown fetch failure: offline, aborted, or unreachable. */\nexport function normalizeThrown(input: unknown, capability?: string): AppError {\n if (typeof navigator !== 'undefined' && navigator.onLine === false) {\n return build(ERROR_CODES.CLIENT_001, undefined, undefined, capability);\n }\n if (input instanceof DOMException && input.name === 'AbortError') {\n return build(ERROR_CODES.NET_001, undefined, undefined, capability);\n }\n return build(ERROR_CODES.NET_001, undefined, undefined, capability);\n}\n\n/** Display string for a failure surface: capability copy plus the code. */\nexport function formatAppError(error: AppError): string {\n return `${error.message} (Reference: ${error.code})`;\n}\n","import { appErrorFromResponse, formatAppError } from '../errors/normalize';\n\nexport interface ApiClientConfig {\n /** localStorage key namespace, e.g. `tt` or `tides`. */\n storagePrefix: string;\n /** Backend refresh endpoint, e.g. `/api/turtletime/auth/refresh/`. */\n refreshPath: string;\n /** Overrides `NEXT_PUBLIC_API_URL` / `http://localhost:8000`. */\n baseUrl?: string;\n accessTokenKey?: string;\n refreshTokenKey?: string;\n workspaceKey?: string;\n /** Suite-wide workspace cookie name. */\n workspaceCookieName?: string;\n /** Overrides `NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN`. */\n workspaceCookieDomain?: string;\n /** Fired when the active workspace changes in this tab. */\n onWorkspaceChange?: (id: string | null) => void;\n /** sessionStorage key prefixes to clear on sign-out. */\n sessionStorageClearPrefixes?: string[];\n}\n\nexport type ApiFetch = (\n path: string,\n options?: RequestInit,\n) => Promise<Response>;\n\n/**\n * Build a browser API client scoped to one ShellStack app. Every app shares the\n * same fetch/refresh/workspace behaviour; only the storage namespace and\n * refresh path differ.\n */\nexport function createApiClient(config: ApiClientConfig) {\n const API_URL =\n config.baseUrl ??\n process.env.NEXT_PUBLIC_API_URL ??\n 'http://localhost:8000';\n\n // Suite-wide workspace selection. Cookies (unlike localStorage) are shared\n // across ports/subdomains, so every product app reads the same value. Set\n // NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN (e.g. \".xenide.io\") to share across\n // subdomains in production; leave unset for localhost.\n const SHARED_WORKSPACE_COOKIE =\n config.workspaceCookieName ?? 'xenide_active_workspace_id';\n const SHARED_WORKSPACE_COOKIE_DOMAIN =\n config.workspaceCookieDomain ??\n process.env.NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN;\n\n const ACCESS_KEY = config.accessTokenKey ?? `${config.storagePrefix}_access_token`;\n const REFRESH_KEY =\n config.refreshTokenKey ?? `${config.storagePrefix}_refresh_token`;\n const WORKSPACE_KEY =\n config.workspaceKey ?? `${config.storagePrefix}_active_workspace_id`;\n\n let accessToken: string | null = null;\n let refreshToken: string | null = null;\n let activeWorkspaceId: string | null = null;\n\n let authFailureHandler: (() => void) | null = null;\n let authFailureFired = false;\n let refreshPromise: Promise<boolean> | null = null;\n\n function readSharedWorkspaceId(): string | null {\n if (typeof document === 'undefined') return null;\n const prefix = `${SHARED_WORKSPACE_COOKIE}=`;\n const entry = document.cookie\n .split('; ')\n .find((item) => item.startsWith(prefix));\n if (!entry) return null;\n const raw = entry.slice(prefix.length);\n if (!raw) return null;\n try {\n return decodeURIComponent(raw) || null;\n } catch {\n return raw;\n }\n }\n\n function writeSharedWorkspaceId(id: string | null): void {\n if (typeof document === 'undefined') return;\n const value = id ? encodeURIComponent(id) : '';\n const maxAge = id ? 60 * 60 * 24 * 365 : 0;\n const domain = SHARED_WORKSPACE_COOKIE_DOMAIN\n ? `; domain=${SHARED_WORKSPACE_COOKIE_DOMAIN}`\n : '';\n document.cookie = `${SHARED_WORKSPACE_COOKIE}=${value}; path=/; max-age=${maxAge}; SameSite=Lax${domain}`;\n }\n\n function onAuthFailure(handler: () => void): void {\n authFailureHandler = handler;\n authFailureFired = false;\n }\n\n function triggerAuthFailure(): void {\n if (authFailureFired) return;\n authFailureFired = true;\n authFailureHandler?.();\n }\n\n function setTokens(access: string, refresh: string): void {\n accessToken = access;\n refreshToken = refresh;\n if (typeof window !== 'undefined') {\n window.localStorage.setItem(ACCESS_KEY, access);\n window.localStorage.setItem(REFRESH_KEY, refresh);\n }\n }\n\n function getAccessToken(): string | null {\n if (accessToken) return accessToken;\n if (typeof window !== 'undefined') {\n accessToken = window.localStorage.getItem(ACCESS_KEY);\n }\n return accessToken;\n }\n\n function getRefreshToken(): string | null {\n if (refreshToken) return refreshToken;\n if (typeof window !== 'undefined') {\n refreshToken = window.localStorage.getItem(REFRESH_KEY);\n }\n return refreshToken;\n }\n\n function setActiveWorkspaceId(\n id: string | null,\n options?: { share?: boolean },\n ): void {\n const changed = activeWorkspaceId !== id;\n activeWorkspaceId = id;\n if (typeof window !== 'undefined') {\n if (id) {\n window.localStorage.setItem(WORKSPACE_KEY, id);\n } else {\n window.localStorage.removeItem(WORKSPACE_KEY);\n }\n if (changed) config.onWorkspaceChange?.(id);\n }\n // Only an explicit choice should update the suite-wide selection; a fallback\n // default must not clobber the workspace another app is using.\n if (options?.share) writeSharedWorkspaceId(id);\n }\n\n function getActiveWorkspaceId(): string | null {\n if (activeWorkspaceId) return activeWorkspaceId;\n if (typeof window !== 'undefined') {\n activeWorkspaceId =\n readSharedWorkspaceId() ?? window.localStorage.getItem(WORKSPACE_KEY);\n }\n return activeWorkspaceId;\n }\n\n function restoreWorkspace(): void {\n if (typeof window !== 'undefined') {\n activeWorkspaceId =\n readSharedWorkspaceId() ?? window.localStorage.getItem(WORKSPACE_KEY);\n }\n }\n\n function clearAuth(): void {\n accessToken = null;\n refreshToken = null;\n activeWorkspaceId = null;\n if (typeof window !== 'undefined') {\n window.localStorage.removeItem(ACCESS_KEY);\n window.localStorage.removeItem(REFRESH_KEY);\n window.localStorage.removeItem(WORKSPACE_KEY);\n const prefixes = config.sessionStorageClearPrefixes ?? [];\n if (prefixes.length > 0) {\n try {\n for (let i = window.sessionStorage.length - 1; i >= 0; i -= 1) {\n const key = window.sessionStorage.key(i);\n if (key && prefixes.some((prefix) => key.startsWith(prefix))) {\n window.sessionStorage.removeItem(key);\n }\n }\n } catch {\n // ignore storage access errors\n }\n }\n }\n }\n\n function restoreTokens(): void {\n if (typeof window !== 'undefined') {\n accessToken = window.localStorage.getItem(ACCESS_KEY);\n refreshToken = window.localStorage.getItem(REFRESH_KEY);\n activeWorkspaceId =\n readSharedWorkspaceId() ?? window.localStorage.getItem(WORKSPACE_KEY);\n }\n }\n\n async function refreshAccessToken(): Promise<boolean> {\n if (refreshPromise) return refreshPromise;\n refreshPromise = refreshAccessTokenInternal().finally(() => {\n refreshPromise = null;\n });\n return refreshPromise;\n }\n\n async function refreshAccessTokenInternal(): Promise<boolean> {\n const refresh = getRefreshToken();\n if (!refresh) {\n triggerAuthFailure();\n return false;\n }\n\n try {\n const res = await fetch(`${API_URL}${config.refreshPath}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({ refresh }),\n });\n if (!res.ok) {\n clearAuth();\n triggerAuthFailure();\n return false;\n }\n const data = await res.json();\n setTokens(data.access, data.refresh);\n authFailureFired = false;\n return true;\n } catch {\n clearAuth();\n triggerAuthFailure();\n return false;\n }\n }\n\n async function apiFetch(\n path: string,\n options: RequestInit = {},\n ): Promise<Response> {\n const token = getAccessToken();\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(options.headers as Record<string, string>),\n };\n\n const body = options.body;\n const isFormData =\n typeof FormData !== 'undefined' && body instanceof FormData;\n\n if (!isFormData) {\n headers['Content-Type'] = 'application/json';\n }\n\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n const wsId = getActiveWorkspaceId();\n if (wsId) {\n headers['X-Active-Workspace-Id'] = wsId;\n }\n\n const url = path.startsWith('http') ? path : `${API_URL}${path}`;\n\n async function doFetch(): Promise<Response> {\n try {\n return await fetch(url, { ...options, headers });\n } catch {\n throw new Error(\n `Could not reach the API at ${API_URL}. Is the Django backend running?`,\n );\n }\n }\n\n let res = await doFetch();\n\n if (res.status === 401 && token) {\n const refreshed = await refreshAccessToken();\n if (refreshed) {\n const newToken = getAccessToken();\n if (newToken) {\n headers['Authorization'] = `Bearer ${newToken}`;\n }\n res = await doFetch();\n if (res.status === 401) {\n triggerAuthFailure();\n }\n }\n }\n\n return res;\n }\n\n async function parseApiError(res: Response): Promise<string> {\n // Never render upstream text or a status number: normalise, then show the code.\n return formatAppError(await appErrorFromResponse(res));\n }\n\n const api = {\n get: (path: string) => apiFetch(path),\n\n post: (path: string, body?: unknown, options: RequestInit = {}) =>\n apiFetch(path, {\n ...options,\n method: 'POST',\n body:\n body instanceof FormData\n ? body\n : body\n ? JSON.stringify(body)\n : undefined,\n }),\n\n put: (path: string, body?: unknown) =>\n apiFetch(path, {\n method: 'PUT',\n body:\n body instanceof FormData\n ? body\n : body\n ? JSON.stringify(body)\n : undefined,\n }),\n\n patch: (path: string, body?: unknown) =>\n apiFetch(path, {\n method: 'PATCH',\n body:\n body instanceof FormData\n ? body\n : body\n ? JSON.stringify(body)\n : undefined,\n }),\n\n delete: (path: string) => apiFetch(path, { method: 'DELETE' }),\n };\n\n return {\n API_URL,\n onAuthFailure,\n setTokens,\n getAccessToken,\n getRefreshToken,\n setActiveWorkspaceId,\n getActiveWorkspaceId,\n restoreWorkspace,\n clearAuth,\n restoreTokens,\n refreshAccessToken,\n parseApiError,\n api,\n };\n}\n\nexport type ApiClient = ReturnType<typeof createApiClient>;\nexport type Api = ApiClient['api'];\n","export interface AuthReturnPathConfig {\n /** sessionStorage key holding the in-app return path, e.g. `tt_auth_return_to`. */\n returnKey: string;\n /** sessionStorage key caching the portal handoff payload. */\n handoffKey?: string;\n defaultNext?: string;\n portalCallbackPath?: string;\n redeemPath?: string;\n}\n\nexport interface PortalHandoffPayload {\n code?: string;\n token?: string;\n access?: string;\n refresh?: string;\n}\n\nexport interface PortalHandoffRead {\n code: string | null;\n token: string | null;\n refresh: string | null;\n next: string;\n}\n\nexport type AuthReturnPath = ReturnType<typeof createAuthReturnPath>;\n\nexport function createAuthReturnPath(config: AuthReturnPathConfig) {\n const RETURN_KEY = config.returnKey;\n const HANDOFF_KEY = config.handoffKey ?? 'ss_portal_handoff';\n const DEFAULT_NEXT = config.defaultNext ?? '/today';\n const PORTAL_CALLBACK_PATH =\n config.portalCallbackPath ?? '/auth/portal/callback';\n const REDEEM_PATH = config.redeemPath ?? '/api/portal/auth/switch/redeem';\n\n function isSafeAuthReturnPath(\n path: string | null | undefined,\n ): path is string {\n if (\n !path ||\n path[0] !== '/' ||\n path.startsWith('//') ||\n path.startsWith('/\\\\')\n ) {\n return false;\n }\n if (path.includes('\\\\') || path.includes('://')) return false;\n return true;\n }\n\n function stashAuthReturnPath(path: string | null | undefined): void {\n if (typeof window === 'undefined' || !isSafeAuthReturnPath(path)) return;\n sessionStorage.setItem(RETURN_KEY, path);\n }\n\n function portalHandoffUrl(\n base: string,\n data: PortalHandoffPayload,\n next?: string,\n fallbackNext = DEFAULT_NEXT,\n ): string {\n const dest = isSafeAuthReturnPath(next) ? next : fallbackNext;\n const hash = new URLSearchParams();\n if (data.code) {\n hash.set('code', data.code);\n } else {\n const token = data.token || data.access || '';\n if (token) hash.set('token', token);\n if (data.refresh) hash.set('refresh', data.refresh);\n }\n hash.set('next', dest);\n return `${base.replace(/\\/$/, '')}${PORTAL_CALLBACK_PATH}#${hash.toString()}`;\n }\n\n function cachedHandoff(fallbackNext: string): PortalHandoffRead | null {\n try {\n const raw = sessionStorage.getItem(HANDOFF_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as PortalHandoffRead;\n if (!parsed.code && !parsed.token) return null;\n return {\n code: parsed.code || null,\n token: parsed.token || null,\n refresh: parsed.refresh || null,\n next: isSafeAuthReturnPath(parsed.next) ? parsed.next : fallbackNext,\n };\n } catch {\n sessionStorage.removeItem(HANDOFF_KEY);\n return null;\n }\n }\n\n function readPortalHandoff(fallbackNext = DEFAULT_NEXT): PortalHandoffRead {\n const cached = cachedHandoff(fallbackNext);\n if (cached) return cached;\n const hash = new URLSearchParams(window.location.hash.replace(/^#/, ''));\n const query = new URLSearchParams(window.location.search);\n const nextRaw = hash.get('next') || query.get('next');\n const result: PortalHandoffRead = {\n code: hash.get('code') || query.get('code'),\n token: hash.get('token') || query.get('token'),\n refresh: hash.get('refresh') || query.get('refresh'),\n next: isSafeAuthReturnPath(nextRaw) ? nextRaw : fallbackNext,\n };\n if (result.code || result.token) {\n sessionStorage.setItem(HANDOFF_KEY, JSON.stringify(result));\n }\n // The callback's hard location.replace removes the fragment from history.\n // Calling history.replaceState here makes Next.js remount the callback before\n // its one-time code redemption has completed.\n return result;\n }\n\n let redeemInflight: Promise<PortalHandoffRead> | null = null;\n\n async function redeemPortalHandoff(\n apiUrl: string,\n fallbackNext = DEFAULT_NEXT,\n ): Promise<PortalHandoffRead> {\n redeemInflight ??= (async () => {\n const { code, token, refresh, next } = readPortalHandoff(fallbackNext);\n if (!code) return { code: null, token, refresh, next };\n const response = await fetch(`${apiUrl.replace(/\\/$/, '')}${REDEEM_PATH}`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ code }),\n });\n if (!response.ok) return { code: null, token: null, refresh: null, next };\n const data = (await response.json()) as PortalHandoffPayload;\n sessionStorage.removeItem(HANDOFF_KEY);\n return {\n code: null,\n token: data.token || data.access || null,\n refresh: data.refresh || null,\n next,\n };\n })();\n return redeemInflight;\n }\n\n function readAuthReturnPath(): string | null {\n if (typeof window === 'undefined') return null;\n const path = sessionStorage.getItem(RETURN_KEY);\n return isSafeAuthReturnPath(path) ? path : null;\n }\n\n function signInHrefWithReturn(fallbackRedirect?: string | null): string {\n const path = isSafeAuthReturnPath(fallbackRedirect)\n ? fallbackRedirect\n : readAuthReturnPath();\n if (!path) return '/login';\n return `/login?redirect=${encodeURIComponent(path)}`;\n }\n\n function registerHrefWithReturn(returnPath: string): string {\n return `/register?redirect=${encodeURIComponent(returnPath)}`;\n }\n\n return {\n isSafeAuthReturnPath,\n stashAuthReturnPath,\n portalHandoffUrl,\n readPortalHandoff,\n redeemPortalHandoff,\n readAuthReturnPath,\n signInHrefWithReturn,\n registerHrefWithReturn,\n };\n}\n","/** Copy and helpers for workspace seats vs per-app access (shared core model). */\n\nexport const WORKSPACE_SEAT_HELP =\n 'Each member uses one workspace seat. Inviting someone reserves a seat until the invite is revoked or accepted.';\n\nexport const ORGANISATION_SEAT_HELP =\n 'Each organisation member uses one seat across all your workspaces. One invite gives them access to every workspace in your organisation.';\n\nexport const TIDES_ACCESS_HELP = 'Tides — project management.';\n\nexport const TURTLETIME_ACCESS_HELP =\n 'TurtleTime — time tracking add-on (Basic plan and above).';\n\nexport const FREE_PLAN_SEAT_HELP =\n 'Free personal workspaces include one product app only. Create an organisation on ShellStack for both apps and integration.';\n\nexport function hasAppAccess(\n canAccessTurtletime: boolean,\n canAccessTides: boolean,\n): boolean {\n return canAccessTurtletime || canAccessTides;\n}\n\nexport function canDisableAppAccess(\n app: 'turtletime' | 'tides',\n canAccessTurtletime: boolean,\n canAccessTides: boolean,\n): boolean {\n if (app === 'turtletime') return canAccessTides;\n return canAccessTurtletime;\n}\n","/** Per-workspace subscription tier helpers (mirrors core/subscriptions.py). */\n\nexport type SubscriptionTier =\n | 'free'\n | 'plus'\n | 'ai_plus'\n | 'basic'\n | 'pro'\n | 'enterprise';\n\nexport interface WorkspaceSubscription {\n tier: SubscriptionTier;\n tier_label: string;\n limits: {\n seats?: number | null;\n integrated_seats: number | null;\n tides_only_seats: number | null;\n };\n usage: {\n seats?: number;\n integrated_seats: number;\n tides_only_seats: number;\n };\n}\n\nexport type SeatType = 'tides' | 'integrated';\n\nconst TIER_LABELS: Record<SubscriptionTier, string> = {\n free: 'Free',\n plus: 'Plus',\n ai_plus: 'AI Plus',\n basic: 'Basic',\n pro: 'Pro',\n enterprise: 'Enterprise',\n};\n\nexport function tierLabel(tier: SubscriptionTier): string {\n if (tier === 'basic' || tier === 'pro') return 'Plus';\n return TIER_LABELS[tier] ?? 'Free';\n}\n\nexport function resolveSubscriptionTier(\n workspace:\n | {\n subscription?: { tier?: string } | null;\n subscription_tier?: string | null;\n }\n | null\n | undefined,\n): SubscriptionTier {\n const tier = workspace?.subscription?.tier ?? workspace?.subscription_tier;\n if (\n tier === 'plus' ||\n tier === 'ai_plus' ||\n tier === 'basic' ||\n tier === 'pro' ||\n tier === 'enterprise' ||\n tier === 'free'\n ) {\n return tier === 'basic' || tier === 'pro' ? 'plus' : tier;\n }\n return 'free';\n}\n\nexport function tierAllowsTurtletime(tier: SubscriptionTier): boolean {\n return tier !== 'free';\n}\n\nexport function defaultInviteSeatType(tier: SubscriptionTier): SeatType {\n return tierAllowsTurtletime(tier) ? 'integrated' : 'tides';\n}\n\nexport function tierAllowsTidesOnlySeats(): boolean {\n return false;\n}\n\nexport function seatTypeFromFlags(\n canAccessTurtletime: boolean,\n canAccessTides: boolean,\n): SeatType {\n if (canAccessTurtletime && canAccessTides) return 'integrated';\n return 'tides';\n}\n\nexport function flagsFromSeatType(seatType: SeatType): {\n can_access_turtletime: boolean;\n can_access_tides: boolean;\n} {\n if (seatType === 'integrated') {\n return { can_access_turtletime: true, can_access_tides: true };\n }\n return { can_access_turtletime: false, can_access_tides: true };\n}\n\nexport function seatTypeLabel(seatType: SeatType): string {\n return seatType === 'integrated' ? 'Tides + TurtleTime' : 'Tides';\n}\n\nexport function seatTypeDescription(\n tier: SubscriptionTier,\n seatType: SeatType,\n): string {\n if (seatType === 'integrated') {\n return 'Project management and time tracking with TT↔Tides integration.';\n }\n if (!tierAllowsTidesOnlySeats()) {\n return 'Project management included on the Free plan.';\n }\n return 'Project management only — no TurtleTime time tracking.';\n}\n\nexport function planSeatSummary(\n subscription: WorkspaceSubscription | null | undefined,\n): string {\n if (!subscription) return '';\n const { tier, limits, usage } = subscription;\n const seatCap = limits.seats ?? limits.integrated_seats;\n const seatCount = usage.seats ?? usage.integrated_seats;\n if (tier === 'enterprise') {\n return 'Unlimited seats';\n }\n if (seatCap != null) {\n return `${seatCount}/${seatCap} seats`;\n }\n return '';\n}\n","export interface BrandSettingsConfig {\n /** Fallback title when the workspace has not customised one. */\n defaultTitle: string;\n /** Window event dispatched when the brand changes. */\n changedEvent: string;\n}\n\nexport interface WorkspaceBrandPayload {\n brand_logo?: string | null;\n brand_title?: string | null;\n brand_show_title?: boolean;\n}\n\nexport type BrandSettings = ReturnType<typeof createBrandSettings>;\n\nexport function createBrandSettings(config: BrandSettingsConfig) {\n const DEFAULT_BRAND_TITLE = config.defaultTitle;\n const BRAND_SETTINGS_CHANGED = config.changedEvent;\n\n interface WorkspaceBrandState {\n logo: string | null;\n title: string | null;\n showTitle: boolean;\n }\n\n const brandState: WorkspaceBrandState = {\n logo: null,\n title: null,\n showTitle: true,\n };\n\n function notifyBrandChange(): void {\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new Event(BRAND_SETTINGS_CHANGED));\n }\n }\n\n function applyWorkspaceBrand(payload: WorkspaceBrandPayload): void {\n if ('brand_logo' in payload) {\n brandState.logo = payload.brand_logo ?? null;\n }\n if ('brand_title' in payload) {\n const title = payload.brand_title;\n brandState.title = title?.trim() ? title.trim() : null;\n }\n if ('brand_show_title' in payload && payload.brand_show_title !== undefined) {\n brandState.showTitle = payload.brand_show_title;\n }\n notifyBrandChange();\n }\n\n function getBrandLogo(): string | null {\n return brandState.logo;\n }\n\n function getCustomBrandTitle(): string | null {\n return brandState.title;\n }\n\n function getBrandShowTitle(): boolean {\n return brandState.showTitle;\n }\n\n /** Title shown in navigation — null when hidden. */\n function getDisplayBrandTitle(): string | null {\n if (!getBrandShowTitle()) return null;\n const custom = getCustomBrandTitle();\n if (custom === null) return DEFAULT_BRAND_TITLE;\n return custom || DEFAULT_BRAND_TITLE;\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function setBrandLogo(dataUrl: string | null): void {\n brandState.logo = dataUrl;\n notifyBrandChange();\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function setCustomBrandTitle(title: string | null): void {\n brandState.title = title?.trim() ? title.trim() : null;\n notifyBrandChange();\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function setBrandShowTitle(show: boolean): void {\n brandState.showTitle = show;\n notifyBrandChange();\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function resetBrandTitle(): void {\n brandState.title = null;\n brandState.showTitle = true;\n notifyBrandChange();\n }\n\n return {\n DEFAULT_BRAND_TITLE,\n BRAND_SETTINGS_CHANGED,\n /** @deprecated Use BRAND_SETTINGS_CHANGED */\n BRAND_LOGO_CHANGED: BRAND_SETTINGS_CHANGED,\n applyWorkspaceBrand,\n getBrandLogo,\n getCustomBrandTitle,\n getBrandShowTitle,\n getDisplayBrandTitle,\n setBrandLogo,\n setCustomBrandTitle,\n setBrandShowTitle,\n resetBrandTitle,\n };\n}\n","export interface WorkspaceAppearanceConfig {\n /** Window event dispatched when the active workspace changes. */\n changedEvent: string;\n /** localStorage base key for the theme. */\n themeStorageBase: string;\n /** Reads the active workspace id from the app's API client. */\n getActiveWorkspaceId: () => string | null;\n}\n\nexport type WorkspaceAppearanceHelpers = ReturnType<\n typeof createWorkspaceAppearance\n>;\n\nexport function createWorkspaceAppearance(config: WorkspaceAppearanceConfig) {\n const WORKSPACE_CHANGED = config.changedEvent;\n const THEME_STORAGE_BASE = config.themeStorageBase;\n\n function notifyWorkspaceChanged(): void {\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new Event(WORKSPACE_CHANGED));\n }\n }\n\n function scopedStorageKey(\n base: string,\n workspaceId?: string | null,\n ): string {\n const wsId = workspaceId ?? config.getActiveWorkspaceId();\n return wsId ? `${base}:${wsId}` : base;\n }\n\n function getScopedStorageItem(\n base: string,\n workspaceId?: string | null,\n ): string | null {\n if (typeof window === 'undefined') return null;\n try {\n const scoped = localStorage.getItem(scopedStorageKey(base, workspaceId));\n if (scoped !== null) return scoped;\n const wsId = workspaceId ?? config.getActiveWorkspaceId();\n if (wsId) {\n return localStorage.getItem(base);\n }\n return null;\n } catch {\n return null;\n }\n }\n\n function setScopedStorageItem(\n base: string,\n value: string | null,\n workspaceId?: string | null,\n ): void {\n if (typeof window === 'undefined') return;\n const key = scopedStorageKey(base, workspaceId);\n if (value === null) {\n localStorage.removeItem(key);\n } else {\n localStorage.setItem(key, value);\n }\n }\n\n return {\n WORKSPACE_CHANGED,\n THEME_STORAGE_BASE,\n notifyWorkspaceChanged,\n scopedStorageKey,\n getScopedStorageItem,\n setScopedStorageItem,\n };\n}\n","import type { Api } from '../api/create-api-client';\nimport type { WorkspaceBrandPayload } from './create-brand-settings';\n\nexport interface WorkspaceAppearanceApiConfig {\n api: Api;\n parseApiError: (res: Response) => Promise<string>;\n /** Endpoint base, e.g. `/api/turtletime/workspaces/current/appearance/`. */\n basePath: string;\n applyWorkspaceBrand: (payload: WorkspaceBrandPayload) => void;\n}\n\nexport type WorkspaceTheme = 'light' | 'dark';\n\nexport interface WorkspaceAppearance {\n theme: WorkspaceTheme;\n brand_logo: string | null;\n brand_title: string | null;\n brand_show_title: boolean;\n}\n\nexport type WorkspaceAppearancePatch = Partial<\n Pick<WorkspaceAppearance, 'theme' | 'brand_title' | 'brand_show_title'>\n> & {\n brand_logo_file?: File;\n clear_brand_logo?: boolean;\n};\n\nexport class WorkspaceAppearanceError extends Error {\n status: number;\n\n constructor(message: string, status: number) {\n super(message);\n this.status = status;\n }\n}\n\nfunction normalizeTheme(value: unknown): WorkspaceTheme {\n return value === 'light' ? 'light' : 'dark';\n}\n\nfunction parseAppearancePayload(\n data: Record<string, unknown>,\n): WorkspaceAppearance {\n const raw = (data.appearance ?? data) as Record<string, unknown>;\n return {\n theme: normalizeTheme(raw.theme),\n brand_logo: (raw.brand_logo as string | null) ?? null,\n brand_title: (raw.brand_title as string | null) ?? null,\n brand_show_title: Boolean(raw.brand_show_title),\n };\n}\n\nexport function createWorkspaceAppearanceApi(\n config: WorkspaceAppearanceApiConfig,\n) {\n const { api, parseApiError, basePath, applyWorkspaceBrand } = config;\n\n async function fetchWorkspaceAppearance(): Promise<WorkspaceAppearance> {\n const res = await api.get(basePath);\n if (!res.ok) {\n throw new WorkspaceAppearanceError(await parseApiError(res), res.status);\n }\n const data = await res.json();\n return parseAppearancePayload(data);\n }\n\n async function updateWorkspaceAppearance(\n patch: WorkspaceAppearancePatch,\n ): Promise<WorkspaceAppearance> {\n const hasFileUpload = patch.brand_logo_file instanceof File;\n const shouldClearLogo = patch.clear_brand_logo === true;\n\n if (hasFileUpload || shouldClearLogo) {\n const formData = new FormData();\n if (patch.theme) formData.append('theme', patch.theme);\n if (patch.brand_title !== undefined) {\n formData.append('brand_title', patch.brand_title ?? '');\n }\n if (patch.brand_show_title !== undefined) {\n formData.append(\n 'brand_show_title',\n patch.brand_show_title ? 'true' : 'false',\n );\n }\n if (hasFileUpload && patch.brand_logo_file) {\n formData.append('brand_logo', patch.brand_logo_file);\n }\n if (shouldClearLogo) {\n formData.append('clear_brand_logo', 'true');\n }\n\n const res = await api.patch(basePath, formData);\n if (!res.ok) {\n throw new Error(await parseApiError(res));\n }\n const data = await res.json();\n return parseAppearancePayload(data);\n }\n\n const res = await api.patch(basePath, {\n theme: patch.theme,\n brand_title: patch.brand_title,\n brand_show_title: patch.brand_show_title,\n });\n if (!res.ok) {\n throw new Error(await parseApiError(res));\n }\n const data = await res.json();\n return parseAppearancePayload(data);\n }\n\n function cacheWorkspaceAppearanceLocally(\n appearance: WorkspaceAppearance,\n ): void {\n applyWorkspaceBrand({\n brand_logo: appearance.brand_logo,\n brand_title: appearance.brand_title,\n brand_show_title: appearance.brand_show_title,\n });\n }\n\n return {\n WorkspaceAppearanceError,\n fetchWorkspaceAppearance,\n updateWorkspaceAppearance,\n cacheWorkspaceAppearanceLocally,\n };\n}\n","import type { Api } from '../api/create-api-client';\n\nexport type CrossAppSyncTopic = 'time' | 'integration' | 'tasks';\n\nexport interface CrossAppSyncConfig {\n api: Api;\n /** Checkpoint endpoint, e.g. `/api/turtletime/sync-checkpoint/`. */\n checkpointPath: string;\n /** Poll interval while the tab is visible (ms). */\n pollMs?: number;\n}\n\ntype SyncCheckpoint = {\n tasks_synced_at: number;\n time_synced_at: number;\n integration_synced_at: number;\n};\n\nexport type CrossAppSync = ReturnType<typeof createCrossAppSync>;\n\n/**\n * Poll backend checkpoints so sibling apps refresh when data mutates\n * elsewhere. Same-tab instant updates stay the app's responsibility via its\n * own window events.\n */\nexport function createCrossAppSync(config: CrossAppSyncConfig) {\n const pollMs = config.pollMs ?? 5000;\n\n const lastKnown: Record<CrossAppSyncTopic, number> = {\n time: 0,\n integration: 0,\n tasks: 0,\n };\n\n const subscribers: Array<{\n topic?: CrossAppSyncTopic;\n callback: () => void;\n lastSync: number;\n }> = [];\n\n let pollInterval: number | null = null;\n let pollListeners = 0;\n\n async function fetchCheckpoint(): Promise<SyncCheckpoint | null> {\n try {\n const res = await config.api.get(config.checkpointPath);\n if (!res.ok) return null;\n return (await res.json()) as SyncCheckpoint;\n } catch {\n return null;\n }\n }\n\n function notify(topic: CrossAppSyncTopic): void {\n for (const sub of subscribers) {\n if (!sub.topic || sub.topic === topic) {\n if (lastKnown[topic] > sub.lastSync) {\n sub.lastSync = lastKnown[topic];\n sub.callback();\n }\n }\n }\n }\n\n function checkAndNotify(data: SyncCheckpoint): void {\n const tasks = data.tasks_synced_at || 0;\n const time = data.time_synced_at || 0;\n const integration = data.integration_synced_at || 0;\n\n if (tasks > lastKnown.tasks) {\n lastKnown.tasks = tasks;\n notify('tasks');\n }\n if (time > lastKnown.time) {\n lastKnown.time = time;\n notify('time');\n }\n if (integration > lastKnown.integration) {\n lastKnown.integration = integration;\n notify('integration');\n }\n }\n\n function startPolling(): void {\n if (pollInterval !== null || typeof window === 'undefined') return;\n\n void fetchCheckpoint().then((data) => data && checkAndNotify(data));\n pollInterval = window.setInterval(() => {\n if (document.visibilityState === 'visible') {\n void fetchCheckpoint().then((data) => data && checkAndNotify(data));\n }\n }, pollMs);\n }\n\n function stopPolling(): void {\n if (pollListeners > 0) return;\n if (pollInterval !== null) {\n window.clearInterval(pollInterval);\n pollInterval = null;\n }\n }\n\n function broadcastCrossAppSync(_topic: CrossAppSyncTopic): void {\n // Cross-subdomain sync is handled by backend checkpoints + polling.\n // Same-tab instant updates are dispatched via the app's own events.\n void _topic;\n }\n\n function subscribeCrossAppSync(\n onSync: () => void,\n topic?: CrossAppSyncTopic,\n ): () => void {\n if (typeof window === 'undefined') return () => {};\n\n const sub = {\n topic,\n callback: onSync,\n lastSync: lastKnown[topic || 'tasks'],\n };\n subscribers.push(sub);\n pollListeners += 1;\n startPolling();\n\n return () => {\n const index = subscribers.indexOf(sub);\n if (index >= 0) subscribers.splice(index, 1);\n pollListeners -= 1;\n if (pollListeners <= 0) stopPolling();\n };\n }\n\n function lastCrossAppSyncTime(topic?: CrossAppSyncTopic): number {\n return topic ? lastKnown[topic] : Math.max(...Object.values(lastKnown));\n }\n\n return {\n broadcastCrossAppSync,\n subscribeCrossAppSync,\n lastCrossAppSyncTime,\n };\n}\n","import type {\n CrossAppSync,\n CrossAppSyncTopic,\n} from './create-cross-app-sync';\n\nexport interface LiveRefreshOptions {\n /** Same-tab custom events (e.g. `tt-timer-mutated`). */\n localEvents?: string[];\n /** Gentle poll while the tab is visible (ms). */\n pollMs?: number;\n /** Limit cross-tab refreshes to one mutation topic. */\n crossAppTopic?: CrossAppSyncTopic;\n}\n\nexport type LiveRefresh = ReturnType<typeof createLiveRefresh>;\n\nexport function createLiveRefresh(sync: CrossAppSync) {\n /** True if a cross-tab sync for the given topic happened after `since`. */\n function hasPendingCrossAppSync(\n topic: CrossAppSyncTopic | undefined,\n since: number,\n ): boolean {\n return sync.lastCrossAppSyncTime(topic) > since;\n }\n\n /** Re-run `refresh` when sibling apps mutate data, on same-tab events, or on an interval. */\n function subscribeLiveRefresh(\n refresh: () => void,\n options: LiveRefreshOptions = {},\n ): () => void {\n if (typeof window === 'undefined') return () => {};\n\n const cleanups: Array<() => void> = [];\n let lastRefreshAt = Date.now();\n\n const trigger = () => {\n lastRefreshAt = Date.now();\n refresh();\n };\n\n for (const eventName of options.localEvents ?? []) {\n window.addEventListener(eventName, trigger);\n cleanups.push(() => window.removeEventListener(eventName, trigger));\n }\n\n cleanups.push(sync.subscribeCrossAppSync(trigger, options.crossAppTopic));\n\n const shouldRefreshOnFocus = () =>\n hasPendingCrossAppSync(options.crossAppTopic, lastRefreshAt);\n\n const onVisibility = () => {\n if (document.visibilityState === 'visible' && shouldRefreshOnFocus()) {\n trigger();\n }\n };\n document.addEventListener('visibilitychange', onVisibility);\n cleanups.push(() =>\n document.removeEventListener('visibilitychange', onVisibility),\n );\n\n const onFocus = () => {\n if (shouldRefreshOnFocus()) {\n trigger();\n }\n };\n window.addEventListener('focus', onFocus);\n cleanups.push(() => window.removeEventListener('focus', onFocus));\n\n if (options.pollMs && options.pollMs > 0) {\n const interval = window.setInterval(() => {\n if (document.visibilityState === 'visible') trigger();\n }, options.pollMs);\n cleanups.push(() => window.clearInterval(interval));\n }\n\n return () => {\n for (const cleanup of cleanups) cleanup();\n };\n }\n\n return { subscribeLiveRefresh, hasPendingCrossAppSync };\n}\n","import type { Api } from '../api/create-api-client';\n\n/** Cross-app (suite) notification from `/api/notifications/` — shared across ShellStack apps. */\nexport interface SuiteNotification {\n id: string;\n title: string;\n body: string;\n href: string;\n kind: string;\n source_app: string;\n read_at: string | null;\n created_at: string;\n workspace: string | null;\n organisation: string | null;\n}\n\nexport interface SuiteNotificationsResponse {\n notifications: SuiteNotification[];\n unread_count: number;\n}\n\nexport interface NotificationsClientConfig {\n api: Api;\n parseApiError: (res: Response) => Promise<string>;\n cacheMs?: number;\n listPath?: string;\n}\n\nexport type NotificationsClient = ReturnType<typeof createNotificationsClient>;\n\nexport function createNotificationsClient(config: NotificationsClientConfig) {\n const { api, parseApiError } = config;\n const CACHE_MS = config.cacheMs ?? 15_000;\n const LIST_PATH = config.listPath ?? '/api/notifications/';\n\n let notificationsCache: {\n data: SuiteNotificationsResponse;\n at: number;\n } | null = null;\n let notificationsInflight: Promise<SuiteNotificationsResponse> | null = null;\n\n async function fetchSuiteNotifications(): Promise<SuiteNotificationsResponse> {\n if (notificationsCache && Date.now() - notificationsCache.at < CACHE_MS) {\n return notificationsCache.data;\n }\n if (notificationsInflight) return notificationsInflight;\n notificationsInflight = (async () => {\n try {\n const res = await api.get(LIST_PATH);\n if (!res.ok) throw new Error(await parseApiError(res));\n const data = (await res.json()) as Partial<SuiteNotificationsResponse>;\n const result: SuiteNotificationsResponse = {\n notifications: data.notifications ?? [],\n unread_count: data.unread_count ?? 0,\n };\n notificationsCache = { data: result, at: Date.now() };\n return result;\n } finally {\n notificationsInflight = null;\n }\n })();\n return notificationsInflight;\n }\n\n async function markSuiteNotificationRead(id: string): Promise<void> {\n const res = await api.post(`${LIST_PATH}${id}/read/`);\n if (!res.ok) throw new Error(await parseApiError(res));\n notificationsCache = null;\n }\n\n async function markAllSuiteNotificationsRead(): Promise<void> {\n const res = await api.post(`${LIST_PATH}read-all/`);\n if (!res.ok) throw new Error(await parseApiError(res));\n notificationsCache = null;\n }\n\n return {\n fetchSuiteNotifications,\n markSuiteNotificationRead,\n markAllSuiteNotificationsRead,\n };\n}\n","export type SuiteAiJobEvent = {\n sequence: number;\n event_type:\n | 'queued'\n | 'started'\n | 'progress'\n | 'tool'\n | 'completed'\n | 'failed'\n | 'cancelled';\n summary: string;\n payload: Record<string, unknown>;\n created_at: string;\n};\n\nexport type SuiteAiJob = {\n id: string;\n status: 'pending' | 'running' | 'completed' | 'failed';\n events: SuiteAiJobEvent[];\n};\n\nexport function activeSuiteAiJobs(jobs: SuiteAiJob[]) {\n return jobs.filter(\n (job) => job.status === 'pending' || job.status === 'running',\n );\n}\n\nexport function sortedSuiteAiEvents(events: SuiteAiJobEvent[]) {\n return [...events].sort((a, b) => a.sequence - b.sequence);\n}\n\nexport function suiteAiEventSummary(event?: SuiteAiJobEvent) {\n const summary = event?.summary\n // eslint-disable-next-line no-control-regex\n .replace(/[\\u0000-\\u001f]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n if (summary) return summary;\n if (event?.event_type === 'queued') return 'Shelly request queued';\n if (event?.event_type === 'started')\n return 'Shelly started preparing a reply';\n if (event?.event_type === 'completed') return 'Shelly reply ready';\n if (event?.event_type === 'failed') return 'AI request failed';\n return 'Shelly is working';\n}\n","export const WEBHOOK_ACTIVITY_ACTIONS = [\n { id: 'created', label: 'Task created' },\n { id: 'updated', label: 'Task updated' },\n { id: 'status_changed', label: 'Task status changed' },\n { id: 'assigned', label: 'Task assigned' },\n { id: 'commented', label: 'Comment posted' },\n { id: 'project_renamed', label: 'Project renamed' },\n { id: 'project_status_changed', label: 'Project archived or restored' },\n] as const;\n\nconst ALL_WEBHOOK_ACTION_IDS = WEBHOOK_ACTIVITY_ACTIONS.map(\n (action) => action.id,\n);\n\nexport function webhookActionsToSelected(\n stored: string[] | null | undefined,\n): Set<string> {\n if (!stored || stored.length === 0) {\n return new Set(ALL_WEBHOOK_ACTION_IDS);\n }\n return new Set(stored);\n}\n\nexport function selectedToWebhookActions(selected: Set<string>): string[] {\n if (selected.size >= ALL_WEBHOOK_ACTION_IDS.length) {\n return [];\n }\n return ALL_WEBHOOK_ACTION_IDS.filter((id) => selected.has(id));\n}\n","export interface JsonLdConfig {\n name: string;\n url: string;\n description: string;\n applicationCategory?: string;\n applicationSubCategory?: string;\n operatingSystem?: string;\n softwareVersion?: string;\n price?: string;\n priceCurrency?: string;\n inLanguage?: string;\n publisherName?: string;\n publisherUrl?: string;\n}\n\n/** Build a schema.org SoftwareApplication JSON-LD string for a product page. */\nexport function createJsonLd(config: JsonLdConfig): string {\n const publisherName = config.publisherName ?? 'Xenide';\n const publisherUrl = config.publisherUrl ?? 'https://xenide.io';\n\n return JSON.stringify({\n '@context': 'https://schema.org',\n '@type': 'SoftwareApplication',\n name: config.name,\n url: config.url,\n description: config.description,\n applicationCategory: config.applicationCategory ?? 'BusinessApplication',\n ...(config.applicationSubCategory\n ? { applicationSubCategory: config.applicationSubCategory }\n : {}),\n operatingSystem: config.operatingSystem ?? 'Web',\n browserRequirements: 'Requires JavaScript. Requires HTML5.',\n softwareVersion: config.softwareVersion ?? '1.0',\n offers: {\n '@type': 'Offer',\n price: config.price ?? '0',\n priceCurrency: config.priceCurrency ?? 'AUD',\n },\n publisher: {\n '@type': 'Organization',\n name: publisherName,\n url: publisherUrl,\n },\n author: {\n '@type': 'Organization',\n name: publisherName,\n url: publisherUrl,\n },\n inLanguage: config.inLanguage ?? 'en-AU',\n });\n}\n","'use client';\n\nimport { useEffect, useState } from 'react';\n\nconst NIGHT_START_HOUR = 18;\nconst NIGHT_END_HOUR = 6;\n\nexport function useIsNight(): boolean {\n const [isNight, setIsNight] = useState(false);\n\n useEffect(() => {\n const check = () => {\n const hour = new Date().getHours();\n return hour >= NIGHT_START_HOUR || hour < NIGHT_END_HOUR;\n };\n setIsNight(check());\n\n let timeout: ReturnType<typeof setTimeout>;\n const update = () => {\n const next = check();\n setIsNight((current) => (current !== next ? next : current));\n timeout = setTimeout(update, 60_000);\n };\n timeout = setTimeout(update, 60_000);\n return () => clearTimeout(timeout);\n }, []);\n\n return isNight;\n}\n"],"mappings":"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","names":["res","data"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors/codes.ts","../src/errors/normalize.ts","../src/api/create-api-client.ts","../src/auth/create-auth-return-path.ts","../src/workspace/access.ts","../src/workspace/subscription.ts","../src/workspace/create-brand-settings.ts","../src/workspace/create-workspace-appearance.ts","../src/workspace/create-workspace-appearance-api.ts","../src/sync/create-cross-app-sync.ts","../src/sync/create-live-refresh.ts","../src/notifications/create-notifications-client.ts","../src/ai/suite-ai-job.ts","../src/webhooks/webhook-activity-actions.ts","../src/seo/create-json-ld.ts","../src/hooks/use-is-night.ts"],"sourcesContent":["export * from './errors';\nexport * from './api/create-api-client';\nexport * from './auth/create-auth-return-path';\nexport * from './workspace/access';\nexport * from './workspace/subscription';\nexport * from './workspace/create-brand-settings';\nexport * from './workspace/create-workspace-appearance';\nexport * from './workspace/create-workspace-appearance-api';\nexport * from './sync/create-cross-app-sync';\nexport * from './sync/create-live-refresh';\nexport * from './notifications/create-notifications-client';\nexport * from './ai/suite-ai-job';\nexport * from './webhooks/webhook-activity-actions';\nexport * from './seo/create-json-ld';\nexport * from './hooks/use-is-night';\n","// AUTO-GENERATED from ERROR_CODES.md by scripts/error-codes/generate.py. Do not edit.\n\nexport const ERROR_CODES = {\n AUTH_001: 'E-AUTH-001',\n AUTH_002: 'E-AUTH-002',\n AUTH_003: 'E-AUTH-003',\n VALID_001: 'E-VALID-001',\n DATA_001: 'E-DATA-001',\n DATA_002: 'E-DATA-002',\n LIMIT_001: 'E-LIMIT-001',\n NET_001: 'E-NET-001',\n EXT_001: 'E-EXT-001',\n EXT_002: 'E-EXT-002',\n SYS_001: 'E-SYS-001',\n SYS_002: 'E-SYS-002',\n CLIENT_001: 'E-CLIENT-001',\n CLIENT_002: 'E-CLIENT-002',\n} as const;\n\nexport type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];\n\nexport const DEFAULT_ERROR_CODE: ErrorCode = 'E-SYS-001';\n\nexport const ERROR_COPY: Record<ErrorCode, string> = {\n 'E-AUTH-001': 'Sign-in was not accepted. Check your details and try again.',\n 'E-AUTH-002': 'Your session has ended. Sign in again to continue.',\n 'E-AUTH-003': 'You do not have access to this. Ask an administrator for access.',\n 'E-VALID-001': 'Some details need correcting before this can be saved.',\n 'E-DATA-001': 'We could not find what you asked for. It may have been removed.',\n 'E-DATA-002': 'This conflicts with existing information. Review it and try again.',\n 'E-LIMIT-001': 'Too many requests right now. Wait a moment and try again.',\n 'E-NET-001': 'We could not reach the service. Try again in a moment.',\n 'E-EXT-001': 'The <capability> is unavailable right now. Try again shortly.',\n 'E-EXT-002': 'The <capability> could not complete that request. Try again, or adjust the input.',\n 'E-SYS-001': 'Something went wrong on our side. We have recorded it. Try again.',\n 'E-SYS-002': 'This feature is temporarily unavailable while we carry out maintenance.',\n 'E-CLIENT-001': 'You appear to be offline. Check your connection and try again.',\n 'E-CLIENT-002': 'We could not read the response. Reload the page and try again.',\n};\n\nexport const ERROR_RETRYABLE: Record<ErrorCode, boolean> = {\n 'E-AUTH-001': false,\n 'E-AUTH-002': true,\n 'E-AUTH-003': false,\n 'E-VALID-001': false,\n 'E-DATA-001': false,\n 'E-DATA-002': false,\n 'E-LIMIT-001': true,\n 'E-NET-001': true,\n 'E-EXT-001': true,\n 'E-EXT-002': true,\n 'E-SYS-001': true,\n 'E-SYS-002': false,\n 'E-CLIENT-001': true,\n 'E-CLIENT-002': true,\n};\n\nconst ERROR_CODE_SET = new Set<string>(Object.values(ERROR_CODES));\n\nexport function isErrorCode(value: unknown): value is ErrorCode {\n return typeof value === 'string' && ERROR_CODE_SET.has(value);\n}\n\nexport function copyFor(code: ErrorCode, capability?: string): string {\n const copy = ERROR_COPY[code] ?? ERROR_COPY[DEFAULT_ERROR_CODE];\n if (copy.includes('<capability>')) {\n return copy.replace('<capability>', capability ?? 'service');\n }\n return copy;\n}\n","import {\n DEFAULT_ERROR_CODE,\n ERROR_CODES,\n ERROR_RETRYABLE,\n copyFor,\n isErrorCode,\n type ErrorCode,\n} from './codes';\n\n/**\n * The single normalised shape every caught failure is reduced to before any\n * component sees it. `message` is always resolved from the local registry;\n * upstream text is never carried through.\n */\nexport type AppError = {\n code: ErrorCode;\n message: string;\n correlationId?: string;\n retryable: boolean;\n};\n\nconst STATUS_TO_CODE: Record<number, ErrorCode> = {\n 400: ERROR_CODES.VALID_001,\n 401: ERROR_CODES.AUTH_001,\n 403: ERROR_CODES.AUTH_003,\n 404: ERROR_CODES.DATA_001,\n 405: ERROR_CODES.SYS_001,\n 409: ERROR_CODES.DATA_002,\n 422: ERROR_CODES.VALID_001,\n 429: ERROR_CODES.LIMIT_001,\n 500: ERROR_CODES.SYS_001,\n 502: ERROR_CODES.EXT_002,\n 503: ERROR_CODES.NET_001,\n};\n\nexport function codeForStatus(status: number): ErrorCode {\n if (STATUS_TO_CODE[status]) return STATUS_TO_CODE[status];\n if (status >= 500) return ERROR_CODES.SYS_001;\n if (status >= 400) return ERROR_CODES.VALID_001;\n return ERROR_CODES.SYS_001;\n}\n\nfunction build(\n code: ErrorCode,\n correlationId?: string,\n retryable?: boolean,\n capability?: string,\n): AppError {\n return {\n code,\n message: copyFor(code, capability),\n ...(correlationId ? { correlationId } : {}),\n retryable: retryable ?? ERROR_RETRYABLE[code],\n };\n}\n\nfunction extractEnvelope(\n input: unknown,\n): { code: string; correlationId?: string; retryable?: boolean } | null {\n if (typeof input !== 'object' || input === null) return null;\n const error = (input as Record<string, unknown>).error;\n if (typeof error !== 'object' || error === null) return null;\n const record = error as Record<string, unknown>;\n return {\n code: typeof record.code === 'string' ? record.code : '',\n correlationId:\n typeof record.correlation_id === 'string'\n ? record.correlation_id\n : undefined,\n retryable:\n typeof record.retryable === 'boolean' ? record.retryable : undefined,\n };\n}\n\n/**\n * Reduce an unknown value to an `AppError`. Accepts only the envelope shape;\n * every other key on the input is discarded. Unknown or absent codes fall back.\n */\nexport function normalizeError(\n input: unknown,\n fallback: ErrorCode = DEFAULT_ERROR_CODE,\n capability?: string,\n): AppError {\n const envelope = extractEnvelope(input);\n if (!envelope) return build(fallback, undefined, undefined, capability);\n const resolved = isErrorCode(envelope.code) ? envelope.code : fallback;\n return build(\n resolved,\n envelope.correlationId,\n envelope.retryable,\n capability,\n );\n}\n\n/** Normalise a non-2xx `Response` without ever reading its raw message. */\nexport async function appErrorFromResponse(\n response: Response,\n capability?: string,\n): Promise<AppError> {\n const fallback = codeForStatus(response.status);\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = undefined;\n }\n return normalizeError(body, fallback, capability);\n}\n\n/** Normalise a thrown fetch failure: offline, aborted, or unreachable. */\nexport function normalizeThrown(input: unknown, capability?: string): AppError {\n if (typeof navigator !== 'undefined' && navigator.onLine === false) {\n return build(ERROR_CODES.CLIENT_001, undefined, undefined, capability);\n }\n if (input instanceof DOMException && input.name === 'AbortError') {\n return build(ERROR_CODES.NET_001, undefined, undefined, capability);\n }\n return build(ERROR_CODES.NET_001, undefined, undefined, capability);\n}\n\n/** Display string for a failure surface: capability copy plus the code. */\nexport function formatAppError(error: AppError): string {\n return `${error.message} (Reference: ${error.code})`;\n}\n","import { appErrorFromResponse, formatAppError } from '../errors/normalize';\n\nexport type ApiFetchInit = RequestInit & { skipAuth?: boolean };\n\nexport interface ApiClientConfig {\n /** localStorage key namespace, e.g. `tt` or `tides`. */\n storagePrefix: string;\n /** Backend refresh endpoint, e.g. `/api/turtletime/auth/refresh/`. */\n refreshPath: string;\n /** Overrides `NEXT_PUBLIC_API_URL` / `http://localhost:8000`. */\n baseUrl?: string;\n accessTokenKey?: string;\n refreshTokenKey?: string;\n workspaceKey?: string;\n /** Suite-wide workspace cookie name. */\n workspaceCookieName?: string;\n /** Overrides `NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN`. */\n workspaceCookieDomain?: string;\n /** Fired when the active workspace changes in this tab. */\n onWorkspaceChange?: (id: string | null) => void;\n /** Fired during sign-out before storage is cleared. */\n onSignOut?: () => void;\n /** Clear the workspace key on sign-out (default `true`). */\n clearWorkspaceOnSignOut?: boolean;\n /** Extra localStorage keys removed on sign-out (e.g. portal app tokens). */\n extraStorageKeysOnClear?: string[];\n /** sessionStorage key prefixes to clear on sign-out. */\n sessionStorageClearPrefixes?: string[];\n /** Refuse to send the session to an unexpected absolute origin. */\n enforceSameOrigin?: boolean;\n}\n\n/**\n * Build a browser API client scoped to one ShellStack app. Every app shares the\n * same fetch/refresh/workspace behaviour; only the storage namespace and\n * refresh path differ.\n */\nexport function createApiClient(config: ApiClientConfig) {\n const API_URL =\n config.baseUrl ??\n process.env.NEXT_PUBLIC_API_URL ??\n 'http://localhost:8000';\n\n // Suite-wide workspace selection. Cookies (unlike localStorage) are shared\n // across ports/subdomains, so every product app reads the same value. Set\n // NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN (e.g. \".xenide.io\") to share across\n // subdomains in production; leave unset for localhost.\n const SHARED_WORKSPACE_COOKIE =\n config.workspaceCookieName ?? 'xenide_active_workspace_id';\n const SHARED_WORKSPACE_COOKIE_DOMAIN =\n config.workspaceCookieDomain ??\n process.env.NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN;\n\n const ACCESS_KEY = config.accessTokenKey ?? `${config.storagePrefix}_access_token`;\n const REFRESH_KEY =\n config.refreshTokenKey ?? `${config.storagePrefix}_refresh_token`;\n const WORKSPACE_KEY =\n config.workspaceKey ?? `${config.storagePrefix}_active_workspace_id`;\n const CLEAR_WORKSPACE = config.clearWorkspaceOnSignOut ?? true;\n\n let accessToken: string | null = null;\n let refreshToken: string | null = null;\n let activeWorkspaceId: string | null = null;\n\n let authFailureHandler: (() => void) | null = null;\n let authFailureFired = false;\n let refreshPromise: Promise<boolean> | null = null;\n\n function readSharedWorkspaceId(): string | null {\n if (typeof document === 'undefined') return null;\n const prefix = `${SHARED_WORKSPACE_COOKIE}=`;\n const entry = document.cookie\n .split('; ')\n .find((item) => item.startsWith(prefix));\n if (!entry) return null;\n const raw = entry.slice(prefix.length);\n if (!raw) return null;\n try {\n return decodeURIComponent(raw) || null;\n } catch {\n return raw;\n }\n }\n\n function writeSharedWorkspaceId(id: string | null): void {\n if (typeof document === 'undefined') return;\n const value = id ? encodeURIComponent(id) : '';\n const maxAge = id ? 60 * 60 * 24 * 365 : 0;\n const domain = SHARED_WORKSPACE_COOKIE_DOMAIN\n ? `; domain=${SHARED_WORKSPACE_COOKIE_DOMAIN}`\n : '';\n document.cookie = `${SHARED_WORKSPACE_COOKIE}=${value}; path=/; max-age=${maxAge}; SameSite=Lax${domain}`;\n }\n\n function onAuthFailure(handler: () => void): void {\n authFailureHandler = handler;\n authFailureFired = false;\n }\n\n function triggerAuthFailure(): void {\n if (authFailureFired) return;\n authFailureFired = true;\n authFailureHandler?.();\n }\n\n function setTokens(access: string, refresh?: string | null): void {\n accessToken = access;\n authFailureFired = false;\n if (refresh !== undefined) refreshToken = refresh;\n if (typeof window !== 'undefined') {\n window.localStorage.setItem(ACCESS_KEY, access);\n if (refresh) window.localStorage.setItem(REFRESH_KEY, refresh);\n else if (refresh !== undefined) window.localStorage.removeItem(REFRESH_KEY);\n }\n }\n\n function getAccessToken(): string | null {\n if (accessToken) return accessToken;\n if (typeof window !== 'undefined') {\n accessToken = window.localStorage.getItem(ACCESS_KEY);\n }\n return accessToken;\n }\n\n function getRefreshToken(): string | null {\n if (refreshToken) return refreshToken;\n if (typeof window !== 'undefined') {\n refreshToken = window.localStorage.getItem(REFRESH_KEY);\n }\n return refreshToken;\n }\n\n function hasAccessToken(): boolean {\n return Boolean(getAccessToken());\n }\n\n function setActiveWorkspaceId(\n id: string | null,\n options?: { share?: boolean },\n ): void {\n const changed = activeWorkspaceId !== id;\n activeWorkspaceId = id;\n if (typeof window !== 'undefined') {\n if (id) {\n window.localStorage.setItem(WORKSPACE_KEY, id);\n } else {\n window.localStorage.removeItem(WORKSPACE_KEY);\n }\n if (changed) config.onWorkspaceChange?.(id);\n }\n // Only an explicit choice should update the suite-wide selection; a fallback\n // default must not clobber the workspace another app is using.\n if (options?.share) writeSharedWorkspaceId(id);\n }\n\n function getActiveWorkspaceId(): string | null {\n if (activeWorkspaceId) return activeWorkspaceId;\n if (typeof window !== 'undefined') {\n activeWorkspaceId =\n readSharedWorkspaceId() ?? window.localStorage.getItem(WORKSPACE_KEY);\n }\n return activeWorkspaceId;\n }\n\n function restoreWorkspace(): void {\n if (typeof window !== 'undefined') {\n activeWorkspaceId =\n readSharedWorkspaceId() ?? window.localStorage.getItem(WORKSPACE_KEY);\n }\n }\n\n function clearAuth(): void {\n config.onSignOut?.();\n accessToken = null;\n refreshToken = null;\n activeWorkspaceId = null;\n if (typeof window !== 'undefined') {\n window.localStorage.removeItem(ACCESS_KEY);\n window.localStorage.removeItem(REFRESH_KEY);\n if (CLEAR_WORKSPACE) window.localStorage.removeItem(WORKSPACE_KEY);\n for (const key of config.extraStorageKeysOnClear ?? []) {\n window.localStorage.removeItem(key);\n }\n const prefixes = config.sessionStorageClearPrefixes ?? [];\n if (prefixes.length > 0) {\n try {\n for (let i = window.sessionStorage.length - 1; i >= 0; i -= 1) {\n const key = window.sessionStorage.key(i);\n if (key && prefixes.some((prefix) => key.startsWith(prefix))) {\n window.sessionStorage.removeItem(key);\n }\n }\n } catch {\n // ignore storage access errors\n }\n }\n }\n }\n\n function restoreTokens(): void {\n if (typeof window !== 'undefined') {\n accessToken = window.localStorage.getItem(ACCESS_KEY);\n refreshToken = window.localStorage.getItem(REFRESH_KEY);\n activeWorkspaceId =\n readSharedWorkspaceId() ?? window.localStorage.getItem(WORKSPACE_KEY);\n }\n }\n\n async function refreshAccessToken(): Promise<boolean> {\n if (refreshPromise) return refreshPromise;\n refreshPromise = refreshAccessTokenInternal().finally(() => {\n refreshPromise = null;\n });\n return refreshPromise;\n }\n\n async function refreshAccessTokenInternal(): Promise<boolean> {\n const refresh = getRefreshToken();\n if (!refresh) {\n triggerAuthFailure();\n return false;\n }\n\n try {\n const res = await fetch(`${API_URL}${config.refreshPath}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({ refresh }),\n });\n if (!res.ok) {\n clearAuth();\n triggerAuthFailure();\n return false;\n }\n const data = await res.json();\n setTokens(data.access, data.refresh);\n authFailureFired = false;\n return true;\n } catch {\n clearAuth();\n triggerAuthFailure();\n return false;\n }\n }\n\n async function apiFetch(\n path: string,\n init: ApiFetchInit = {},\n ): Promise<Response> {\n const { skipAuth, ...requestInit } = init;\n const token = getAccessToken();\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(requestInit.headers as Record<string, string>),\n };\n\n const body = requestInit.body;\n const isFormData =\n typeof FormData !== 'undefined' && body instanceof FormData;\n\n if (!isFormData) {\n headers['Content-Type'] = 'application/json';\n }\n\n if (token && !skipAuth) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n const wsId = getActiveWorkspaceId();\n if (wsId && !skipAuth) {\n headers['X-Active-Workspace-Id'] = wsId;\n }\n\n const url = path.startsWith('http') ? path : `${API_URL}${path}`;\n if (\n config.enforceSameOrigin &&\n !skipAuth &&\n path.startsWith('http') &&\n !url.startsWith(API_URL)\n ) {\n throw new Error('Refusing to send the session to an unexpected origin.');\n }\n\n async function doFetch(): Promise<Response> {\n try {\n return await fetch(url, { ...requestInit, headers });\n } catch (error) {\n if (\n init.signal?.aborted ||\n (error instanceof DOMException && error.name === 'AbortError')\n ) {\n throw error;\n }\n throw new Error(\n `Could not reach the API at ${API_URL}. Is the Django backend running?`,\n );\n }\n }\n\n let res = await doFetch();\n\n if (res.status === 401 && token && !skipAuth) {\n const refreshed = await refreshAccessToken();\n if (refreshed) {\n const newToken = getAccessToken();\n if (newToken) {\n headers['Authorization'] = `Bearer ${newToken}`;\n }\n res = await doFetch();\n }\n if (res.status === 401) {\n clearAuth();\n triggerAuthFailure();\n }\n }\n\n return res;\n }\n\n async function parseApiError(res: Response): Promise<string> {\n // Never render upstream text or a status number: normalise, then show the code.\n return formatAppError(await appErrorFromResponse(res));\n }\n\n const api = {\n get: (path: string, init?: ApiFetchInit) => apiFetch(path, init),\n\n post: (path: string, body?: unknown, init: ApiFetchInit = {}) =>\n apiFetch(path, {\n ...init,\n method: 'POST',\n body:\n body instanceof FormData\n ? body\n : body\n ? JSON.stringify(body)\n : undefined,\n }),\n\n put: (path: string, body?: unknown, init: ApiFetchInit = {}) =>\n apiFetch(path, {\n ...init,\n method: 'PUT',\n body:\n body instanceof FormData\n ? body\n : body\n ? JSON.stringify(body)\n : undefined,\n }),\n\n patch: (path: string, body?: unknown, init: ApiFetchInit = {}) =>\n apiFetch(path, {\n ...init,\n method: 'PATCH',\n body:\n body instanceof FormData\n ? body\n : body\n ? JSON.stringify(body)\n : undefined,\n }),\n\n delete: (path: string, init: ApiFetchInit = {}) =>\n apiFetch(path, { ...init, method: 'DELETE' }),\n };\n\n return {\n API_URL,\n onAuthFailure,\n setTokens,\n getAccessToken,\n getRefreshToken,\n hasAccessToken,\n setActiveWorkspaceId,\n getActiveWorkspaceId,\n restoreWorkspace,\n clearAuth,\n restoreTokens,\n refreshAccessToken,\n parseApiError,\n api,\n // Aliases used by Kraken/Shelly.\n restoreSession: restoreTokens,\n clearSession: clearAuth,\n };\n}\n\nexport type ApiClient = ReturnType<typeof createApiClient>;\nexport type Api = ApiClient['api'];\n","export interface AuthReturnPathConfig {\n /** sessionStorage key holding the in-app return path, e.g. `tt_auth_return_to`. */\n returnKey: string;\n /** sessionStorage key caching the portal handoff payload. */\n handoffKey?: string;\n defaultNext?: string;\n portalCallbackPath?: string;\n redeemPath?: string;\n}\n\nexport interface PortalHandoffPayload {\n code?: string;\n token?: string;\n access?: string;\n refresh?: string;\n}\n\nexport interface PortalHandoffRead {\n code: string | null;\n token: string | null;\n refresh: string | null;\n next: string;\n}\n\nexport type AuthReturnPath = ReturnType<typeof createAuthReturnPath>;\n\nexport function createAuthReturnPath(config: AuthReturnPathConfig) {\n const RETURN_KEY = config.returnKey;\n const HANDOFF_KEY = config.handoffKey ?? 'ss_portal_handoff';\n const DEFAULT_NEXT = config.defaultNext ?? '/today';\n const PORTAL_CALLBACK_PATH =\n config.portalCallbackPath ?? '/auth/portal/callback';\n const REDEEM_PATH = config.redeemPath ?? '/api/portal/auth/switch/redeem';\n\n function isSafeAuthReturnPath(\n path: string | null | undefined,\n ): path is string {\n if (\n !path ||\n path[0] !== '/' ||\n path.startsWith('//') ||\n path.startsWith('/\\\\')\n ) {\n return false;\n }\n if (path.includes('\\\\') || path.includes('://')) return false;\n return true;\n }\n\n function stashAuthReturnPath(path: string | null | undefined): void {\n if (typeof window === 'undefined' || !isSafeAuthReturnPath(path)) return;\n sessionStorage.setItem(RETURN_KEY, path);\n }\n\n function portalHandoffUrl(\n base: string,\n data: PortalHandoffPayload,\n next?: string,\n fallbackNext = DEFAULT_NEXT,\n ): string {\n const dest = isSafeAuthReturnPath(next) ? next : fallbackNext;\n const hash = new URLSearchParams();\n if (data.code) {\n hash.set('code', data.code);\n } else {\n const token = data.token || data.access || '';\n if (token) hash.set('token', token);\n if (data.refresh) hash.set('refresh', data.refresh);\n }\n hash.set('next', dest);\n return `${base.replace(/\\/$/, '')}${PORTAL_CALLBACK_PATH}#${hash.toString()}`;\n }\n\n function cachedHandoff(fallbackNext: string): PortalHandoffRead | null {\n try {\n const raw = sessionStorage.getItem(HANDOFF_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as PortalHandoffRead;\n if (!parsed.code && !parsed.token) return null;\n return {\n code: parsed.code || null,\n token: parsed.token || null,\n refresh: parsed.refresh || null,\n next: isSafeAuthReturnPath(parsed.next) ? parsed.next : fallbackNext,\n };\n } catch {\n sessionStorage.removeItem(HANDOFF_KEY);\n return null;\n }\n }\n\n function readPortalHandoff(fallbackNext = DEFAULT_NEXT): PortalHandoffRead {\n const cached = cachedHandoff(fallbackNext);\n if (cached) return cached;\n const hash = new URLSearchParams(window.location.hash.replace(/^#/, ''));\n const query = new URLSearchParams(window.location.search);\n const nextRaw = hash.get('next') || query.get('next');\n const result: PortalHandoffRead = {\n code: hash.get('code') || query.get('code'),\n token: hash.get('token') || query.get('token'),\n refresh: hash.get('refresh') || query.get('refresh'),\n next: isSafeAuthReturnPath(nextRaw) ? nextRaw : fallbackNext,\n };\n if (result.code || result.token) {\n sessionStorage.setItem(HANDOFF_KEY, JSON.stringify(result));\n }\n // The callback's hard location.replace removes the fragment from history.\n // Calling history.replaceState here makes Next.js remount the callback before\n // its one-time code redemption has completed.\n return result;\n }\n\n let redeemInflight: Promise<PortalHandoffRead> | null = null;\n\n async function redeemPortalHandoff(\n apiUrl: string,\n fallbackNext = DEFAULT_NEXT,\n ): Promise<PortalHandoffRead> {\n redeemInflight ??= (async () => {\n const { code, token, refresh, next } = readPortalHandoff(fallbackNext);\n if (!code) return { code: null, token, refresh, next };\n const response = await fetch(`${apiUrl.replace(/\\/$/, '')}${REDEEM_PATH}`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ code }),\n });\n if (!response.ok) return { code: null, token: null, refresh: null, next };\n const data = (await response.json()) as PortalHandoffPayload;\n sessionStorage.removeItem(HANDOFF_KEY);\n return {\n code: null,\n token: data.token || data.access || null,\n refresh: data.refresh || null,\n next,\n };\n })();\n return redeemInflight;\n }\n\n function readAuthReturnPath(): string | null {\n if (typeof window === 'undefined') return null;\n const path = sessionStorage.getItem(RETURN_KEY);\n return isSafeAuthReturnPath(path) ? path : null;\n }\n\n function signInHrefWithReturn(fallbackRedirect?: string | null): string {\n const path = isSafeAuthReturnPath(fallbackRedirect)\n ? fallbackRedirect\n : readAuthReturnPath();\n if (!path) return '/login';\n return `/login?redirect=${encodeURIComponent(path)}`;\n }\n\n function registerHrefWithReturn(returnPath: string): string {\n return `/register?redirect=${encodeURIComponent(returnPath)}`;\n }\n\n return {\n isSafeAuthReturnPath,\n stashAuthReturnPath,\n portalHandoffUrl,\n readPortalHandoff,\n redeemPortalHandoff,\n readAuthReturnPath,\n signInHrefWithReturn,\n registerHrefWithReturn,\n };\n}\n","/** Copy and helpers for workspace seats vs per-app access (shared core model). */\n\nexport const WORKSPACE_SEAT_HELP =\n 'Each member uses one workspace seat. Inviting someone reserves a seat until the invite is revoked or accepted.';\n\nexport const ORGANISATION_SEAT_HELP =\n 'Each organisation member uses one seat across all your workspaces. One invite gives them access to every workspace in your organisation.';\n\nexport const TIDES_ACCESS_HELP = 'Tides — project management.';\n\nexport const TURTLETIME_ACCESS_HELP =\n 'TurtleTime — time tracking add-on (Basic plan and above).';\n\nexport const FREE_PLAN_SEAT_HELP =\n 'Free personal workspaces include one product app only. Create an organisation on ShellStack for both apps and integration.';\n\nexport function hasAppAccess(\n canAccessTurtletime: boolean,\n canAccessTides: boolean,\n): boolean {\n return canAccessTurtletime || canAccessTides;\n}\n\nexport function canDisableAppAccess(\n app: 'turtletime' | 'tides',\n canAccessTurtletime: boolean,\n canAccessTides: boolean,\n): boolean {\n if (app === 'turtletime') return canAccessTides;\n return canAccessTurtletime;\n}\n","/** Per-workspace subscription tier helpers (mirrors core/subscriptions.py). */\n\nexport type SubscriptionTier =\n | 'free'\n | 'plus'\n | 'ai_plus'\n | 'basic'\n | 'pro'\n | 'enterprise';\n\nexport interface WorkspaceSubscription {\n tier: SubscriptionTier;\n tier_label: string;\n limits: {\n seats?: number | null;\n integrated_seats: number | null;\n tides_only_seats: number | null;\n };\n usage: {\n seats?: number;\n integrated_seats: number;\n tides_only_seats: number;\n };\n}\n\nexport type SeatType = 'tides' | 'integrated';\n\nconst TIER_LABELS: Record<SubscriptionTier, string> = {\n free: 'Free',\n plus: 'Plus',\n ai_plus: 'AI Plus',\n basic: 'Basic',\n pro: 'Pro',\n enterprise: 'Enterprise',\n};\n\nexport function tierLabel(tier: SubscriptionTier): string {\n if (tier === 'basic' || tier === 'pro') return 'Plus';\n return TIER_LABELS[tier] ?? 'Free';\n}\n\nexport function resolveSubscriptionTier(\n workspace:\n | {\n subscription?: { tier?: string } | null;\n subscription_tier?: string | null;\n }\n | null\n | undefined,\n): SubscriptionTier {\n const tier = workspace?.subscription?.tier ?? workspace?.subscription_tier;\n if (\n tier === 'plus' ||\n tier === 'ai_plus' ||\n tier === 'basic' ||\n tier === 'pro' ||\n tier === 'enterprise' ||\n tier === 'free'\n ) {\n return tier === 'basic' || tier === 'pro' ? 'plus' : tier;\n }\n return 'free';\n}\n\nexport function tierAllowsTurtletime(tier: SubscriptionTier): boolean {\n return tier !== 'free';\n}\n\nexport function defaultInviteSeatType(tier: SubscriptionTier): SeatType {\n return tierAllowsTurtletime(tier) ? 'integrated' : 'tides';\n}\n\nexport function tierAllowsTidesOnlySeats(): boolean {\n return false;\n}\n\nexport function seatTypeFromFlags(\n canAccessTurtletime: boolean,\n canAccessTides: boolean,\n): SeatType {\n if (canAccessTurtletime && canAccessTides) return 'integrated';\n return 'tides';\n}\n\nexport function flagsFromSeatType(seatType: SeatType): {\n can_access_turtletime: boolean;\n can_access_tides: boolean;\n} {\n if (seatType === 'integrated') {\n return { can_access_turtletime: true, can_access_tides: true };\n }\n return { can_access_turtletime: false, can_access_tides: true };\n}\n\nexport function seatTypeLabel(seatType: SeatType): string {\n return seatType === 'integrated' ? 'Tides + TurtleTime' : 'Tides';\n}\n\nexport function seatTypeDescription(\n tier: SubscriptionTier,\n seatType: SeatType,\n): string {\n if (seatType === 'integrated') {\n return 'Project management and time tracking with TT↔Tides integration.';\n }\n if (!tierAllowsTidesOnlySeats()) {\n return 'Project management included on the Free plan.';\n }\n return 'Project management only — no TurtleTime time tracking.';\n}\n\nexport function planSeatSummary(\n subscription: WorkspaceSubscription | null | undefined,\n): string {\n if (!subscription) return '';\n const { tier, limits, usage } = subscription;\n const seatCap = limits.seats ?? limits.integrated_seats;\n const seatCount = usage.seats ?? usage.integrated_seats;\n if (tier === 'enterprise') {\n return 'Unlimited seats';\n }\n if (seatCap != null) {\n return `${seatCount}/${seatCap} seats`;\n }\n return '';\n}\n","export interface BrandSettingsConfig {\n /** Fallback title when the workspace has not customised one. */\n defaultTitle: string;\n /** Window event dispatched when the brand changes. */\n changedEvent: string;\n}\n\nexport interface WorkspaceBrandPayload {\n brand_logo?: string | null;\n brand_title?: string | null;\n brand_show_title?: boolean;\n}\n\nexport type BrandSettings = ReturnType<typeof createBrandSettings>;\n\nexport function createBrandSettings(config: BrandSettingsConfig) {\n const DEFAULT_BRAND_TITLE = config.defaultTitle;\n const BRAND_SETTINGS_CHANGED = config.changedEvent;\n\n interface WorkspaceBrandState {\n logo: string | null;\n title: string | null;\n showTitle: boolean;\n }\n\n const brandState: WorkspaceBrandState = {\n logo: null,\n title: null,\n showTitle: true,\n };\n\n function notifyBrandChange(): void {\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new Event(BRAND_SETTINGS_CHANGED));\n }\n }\n\n function applyWorkspaceBrand(payload: WorkspaceBrandPayload): void {\n if ('brand_logo' in payload) {\n brandState.logo = payload.brand_logo ?? null;\n }\n if ('brand_title' in payload) {\n const title = payload.brand_title;\n brandState.title = title?.trim() ? title.trim() : null;\n }\n if ('brand_show_title' in payload && payload.brand_show_title !== undefined) {\n brandState.showTitle = payload.brand_show_title;\n }\n notifyBrandChange();\n }\n\n function getBrandLogo(): string | null {\n return brandState.logo;\n }\n\n function getCustomBrandTitle(): string | null {\n return brandState.title;\n }\n\n function getBrandShowTitle(): boolean {\n return brandState.showTitle;\n }\n\n /** Title shown in navigation — null when hidden. */\n function getDisplayBrandTitle(): string | null {\n if (!getBrandShowTitle()) return null;\n const custom = getCustomBrandTitle();\n if (custom === null) return DEFAULT_BRAND_TITLE;\n return custom || DEFAULT_BRAND_TITLE;\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function setBrandLogo(dataUrl: string | null): void {\n brandState.logo = dataUrl;\n notifyBrandChange();\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function setCustomBrandTitle(title: string | null): void {\n brandState.title = title?.trim() ? title.trim() : null;\n notifyBrandChange();\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function setBrandShowTitle(show: boolean): void {\n brandState.showTitle = show;\n notifyBrandChange();\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function resetBrandTitle(): void {\n brandState.title = null;\n brandState.showTitle = true;\n notifyBrandChange();\n }\n\n return {\n DEFAULT_BRAND_TITLE,\n BRAND_SETTINGS_CHANGED,\n /** @deprecated Use BRAND_SETTINGS_CHANGED */\n BRAND_LOGO_CHANGED: BRAND_SETTINGS_CHANGED,\n applyWorkspaceBrand,\n getBrandLogo,\n getCustomBrandTitle,\n getBrandShowTitle,\n getDisplayBrandTitle,\n setBrandLogo,\n setCustomBrandTitle,\n setBrandShowTitle,\n resetBrandTitle,\n };\n}\n","export interface WorkspaceAppearanceConfig {\n /** Window event dispatched when the active workspace changes. */\n changedEvent: string;\n /** localStorage base key for the theme. */\n themeStorageBase: string;\n /** Reads the active workspace id from the app's API client. */\n getActiveWorkspaceId: () => string | null;\n}\n\nexport type WorkspaceAppearanceHelpers = ReturnType<\n typeof createWorkspaceAppearance\n>;\n\nexport function createWorkspaceAppearance(config: WorkspaceAppearanceConfig) {\n const WORKSPACE_CHANGED = config.changedEvent;\n const THEME_STORAGE_BASE = config.themeStorageBase;\n\n function notifyWorkspaceChanged(): void {\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new Event(WORKSPACE_CHANGED));\n }\n }\n\n function scopedStorageKey(\n base: string,\n workspaceId?: string | null,\n ): string {\n const wsId = workspaceId ?? config.getActiveWorkspaceId();\n return wsId ? `${base}:${wsId}` : base;\n }\n\n function getScopedStorageItem(\n base: string,\n workspaceId?: string | null,\n ): string | null {\n if (typeof window === 'undefined') return null;\n try {\n const scoped = localStorage.getItem(scopedStorageKey(base, workspaceId));\n if (scoped !== null) return scoped;\n const wsId = workspaceId ?? config.getActiveWorkspaceId();\n if (wsId) {\n return localStorage.getItem(base);\n }\n return null;\n } catch {\n return null;\n }\n }\n\n function setScopedStorageItem(\n base: string,\n value: string | null,\n workspaceId?: string | null,\n ): void {\n if (typeof window === 'undefined') return;\n const key = scopedStorageKey(base, workspaceId);\n if (value === null) {\n localStorage.removeItem(key);\n } else {\n localStorage.setItem(key, value);\n }\n }\n\n return {\n WORKSPACE_CHANGED,\n THEME_STORAGE_BASE,\n notifyWorkspaceChanged,\n scopedStorageKey,\n getScopedStorageItem,\n setScopedStorageItem,\n };\n}\n","import type { Api } from '../api/create-api-client';\nimport type { WorkspaceBrandPayload } from './create-brand-settings';\n\nexport interface WorkspaceAppearanceApiConfig {\n api: Api;\n parseApiError: (res: Response) => Promise<string>;\n /** Endpoint base, e.g. `/api/turtletime/workspaces/current/appearance/`. */\n basePath: string;\n applyWorkspaceBrand: (payload: WorkspaceBrandPayload) => void;\n}\n\nexport type WorkspaceTheme = 'light' | 'dark';\n\nexport interface WorkspaceAppearance {\n theme: WorkspaceTheme;\n brand_logo: string | null;\n brand_title: string | null;\n brand_show_title: boolean;\n}\n\nexport type WorkspaceAppearancePatch = Partial<\n Pick<WorkspaceAppearance, 'theme' | 'brand_title' | 'brand_show_title'>\n> & {\n brand_logo_file?: File;\n clear_brand_logo?: boolean;\n};\n\nexport class WorkspaceAppearanceError extends Error {\n status: number;\n\n constructor(message: string, status: number) {\n super(message);\n this.status = status;\n }\n}\n\nfunction normalizeTheme(value: unknown): WorkspaceTheme {\n return value === 'light' ? 'light' : 'dark';\n}\n\nfunction parseAppearancePayload(\n data: Record<string, unknown>,\n): WorkspaceAppearance {\n const raw = (data.appearance ?? data) as Record<string, unknown>;\n return {\n theme: normalizeTheme(raw.theme),\n brand_logo: (raw.brand_logo as string | null) ?? null,\n brand_title: (raw.brand_title as string | null) ?? null,\n brand_show_title: Boolean(raw.brand_show_title),\n };\n}\n\nexport function createWorkspaceAppearanceApi(\n config: WorkspaceAppearanceApiConfig,\n) {\n const { api, parseApiError, basePath, applyWorkspaceBrand } = config;\n\n async function fetchWorkspaceAppearance(): Promise<WorkspaceAppearance> {\n const res = await api.get(basePath);\n if (!res.ok) {\n throw new WorkspaceAppearanceError(await parseApiError(res), res.status);\n }\n const data = await res.json();\n return parseAppearancePayload(data);\n }\n\n async function updateWorkspaceAppearance(\n patch: WorkspaceAppearancePatch,\n ): Promise<WorkspaceAppearance> {\n const hasFileUpload = patch.brand_logo_file instanceof File;\n const shouldClearLogo = patch.clear_brand_logo === true;\n\n if (hasFileUpload || shouldClearLogo) {\n const formData = new FormData();\n if (patch.theme) formData.append('theme', patch.theme);\n if (patch.brand_title !== undefined) {\n formData.append('brand_title', patch.brand_title ?? '');\n }\n if (patch.brand_show_title !== undefined) {\n formData.append(\n 'brand_show_title',\n patch.brand_show_title ? 'true' : 'false',\n );\n }\n if (hasFileUpload && patch.brand_logo_file) {\n formData.append('brand_logo', patch.brand_logo_file);\n }\n if (shouldClearLogo) {\n formData.append('clear_brand_logo', 'true');\n }\n\n const res = await api.patch(basePath, formData);\n if (!res.ok) {\n throw new Error(await parseApiError(res));\n }\n const data = await res.json();\n return parseAppearancePayload(data);\n }\n\n const res = await api.patch(basePath, {\n theme: patch.theme,\n brand_title: patch.brand_title,\n brand_show_title: patch.brand_show_title,\n });\n if (!res.ok) {\n throw new Error(await parseApiError(res));\n }\n const data = await res.json();\n return parseAppearancePayload(data);\n }\n\n function cacheWorkspaceAppearanceLocally(\n appearance: WorkspaceAppearance,\n ): void {\n applyWorkspaceBrand({\n brand_logo: appearance.brand_logo,\n brand_title: appearance.brand_title,\n brand_show_title: appearance.brand_show_title,\n });\n }\n\n return {\n WorkspaceAppearanceError,\n fetchWorkspaceAppearance,\n updateWorkspaceAppearance,\n cacheWorkspaceAppearanceLocally,\n };\n}\n","import type { Api } from '../api/create-api-client';\n\nexport type CrossAppSyncTopic = 'time' | 'integration' | 'tasks';\n\nexport interface CrossAppSyncConfig {\n api: Api;\n /** Checkpoint endpoint, e.g. `/api/turtletime/sync-checkpoint/`. */\n checkpointPath: string;\n /** Poll interval while the tab is visible (ms). */\n pollMs?: number;\n}\n\ntype SyncCheckpoint = {\n tasks_synced_at: number;\n time_synced_at: number;\n integration_synced_at: number;\n};\n\nexport type CrossAppSync = ReturnType<typeof createCrossAppSync>;\n\n/**\n * Poll backend checkpoints so sibling apps refresh when data mutates\n * elsewhere. Same-tab instant updates stay the app's responsibility via its\n * own window events.\n */\nexport function createCrossAppSync(config: CrossAppSyncConfig) {\n const pollMs = config.pollMs ?? 5000;\n\n const lastKnown: Record<CrossAppSyncTopic, number> = {\n time: 0,\n integration: 0,\n tasks: 0,\n };\n\n const subscribers: Array<{\n topic?: CrossAppSyncTopic;\n callback: () => void;\n lastSync: number;\n }> = [];\n\n let pollInterval: number | null = null;\n let pollListeners = 0;\n\n async function fetchCheckpoint(): Promise<SyncCheckpoint | null> {\n try {\n const res = await config.api.get(config.checkpointPath);\n if (!res.ok) return null;\n return (await res.json()) as SyncCheckpoint;\n } catch {\n return null;\n }\n }\n\n function notify(topic: CrossAppSyncTopic): void {\n for (const sub of subscribers) {\n if (!sub.topic || sub.topic === topic) {\n if (lastKnown[topic] > sub.lastSync) {\n sub.lastSync = lastKnown[topic];\n sub.callback();\n }\n }\n }\n }\n\n function checkAndNotify(data: SyncCheckpoint): void {\n const tasks = data.tasks_synced_at || 0;\n const time = data.time_synced_at || 0;\n const integration = data.integration_synced_at || 0;\n\n if (tasks > lastKnown.tasks) {\n lastKnown.tasks = tasks;\n notify('tasks');\n }\n if (time > lastKnown.time) {\n lastKnown.time = time;\n notify('time');\n }\n if (integration > lastKnown.integration) {\n lastKnown.integration = integration;\n notify('integration');\n }\n }\n\n function startPolling(): void {\n if (pollInterval !== null || typeof window === 'undefined') return;\n\n void fetchCheckpoint().then((data) => data && checkAndNotify(data));\n pollInterval = window.setInterval(() => {\n if (document.visibilityState === 'visible') {\n void fetchCheckpoint().then((data) => data && checkAndNotify(data));\n }\n }, pollMs);\n }\n\n function stopPolling(): void {\n if (pollListeners > 0) return;\n if (pollInterval !== null) {\n window.clearInterval(pollInterval);\n pollInterval = null;\n }\n }\n\n function broadcastCrossAppSync(_topic: CrossAppSyncTopic): void {\n // Cross-subdomain sync is handled by backend checkpoints + polling.\n // Same-tab instant updates are dispatched via the app's own events.\n void _topic;\n }\n\n function subscribeCrossAppSync(\n onSync: () => void,\n topic?: CrossAppSyncTopic,\n ): () => void {\n if (typeof window === 'undefined') return () => {};\n\n const sub = {\n topic,\n callback: onSync,\n lastSync: lastKnown[topic || 'tasks'],\n };\n subscribers.push(sub);\n pollListeners += 1;\n startPolling();\n\n return () => {\n const index = subscribers.indexOf(sub);\n if (index >= 0) subscribers.splice(index, 1);\n pollListeners -= 1;\n if (pollListeners <= 0) stopPolling();\n };\n }\n\n function lastCrossAppSyncTime(topic?: CrossAppSyncTopic): number {\n return topic ? lastKnown[topic] : Math.max(...Object.values(lastKnown));\n }\n\n return {\n broadcastCrossAppSync,\n subscribeCrossAppSync,\n lastCrossAppSyncTime,\n };\n}\n","import type {\n CrossAppSync,\n CrossAppSyncTopic,\n} from './create-cross-app-sync';\n\nexport interface LiveRefreshOptions {\n /** Same-tab custom events (e.g. `tt-timer-mutated`). */\n localEvents?: string[];\n /** Gentle poll while the tab is visible (ms). */\n pollMs?: number;\n /** Limit cross-tab refreshes to one mutation topic. */\n crossAppTopic?: CrossAppSyncTopic;\n}\n\nexport type LiveRefresh = ReturnType<typeof createLiveRefresh>;\n\nexport function createLiveRefresh(sync: CrossAppSync) {\n /** True if a cross-tab sync for the given topic happened after `since`. */\n function hasPendingCrossAppSync(\n topic: CrossAppSyncTopic | undefined,\n since: number,\n ): boolean {\n return sync.lastCrossAppSyncTime(topic) > since;\n }\n\n /** Re-run `refresh` when sibling apps mutate data, on same-tab events, or on an interval. */\n function subscribeLiveRefresh(\n refresh: () => void,\n options: LiveRefreshOptions = {},\n ): () => void {\n if (typeof window === 'undefined') return () => {};\n\n const cleanups: Array<() => void> = [];\n let lastRefreshAt = Date.now();\n\n const trigger = () => {\n lastRefreshAt = Date.now();\n refresh();\n };\n\n for (const eventName of options.localEvents ?? []) {\n window.addEventListener(eventName, trigger);\n cleanups.push(() => window.removeEventListener(eventName, trigger));\n }\n\n cleanups.push(sync.subscribeCrossAppSync(trigger, options.crossAppTopic));\n\n const shouldRefreshOnFocus = () =>\n hasPendingCrossAppSync(options.crossAppTopic, lastRefreshAt);\n\n const onVisibility = () => {\n if (document.visibilityState === 'visible' && shouldRefreshOnFocus()) {\n trigger();\n }\n };\n document.addEventListener('visibilitychange', onVisibility);\n cleanups.push(() =>\n document.removeEventListener('visibilitychange', onVisibility),\n );\n\n const onFocus = () => {\n if (shouldRefreshOnFocus()) {\n trigger();\n }\n };\n window.addEventListener('focus', onFocus);\n cleanups.push(() => window.removeEventListener('focus', onFocus));\n\n if (options.pollMs && options.pollMs > 0) {\n const interval = window.setInterval(() => {\n if (document.visibilityState === 'visible') trigger();\n }, options.pollMs);\n cleanups.push(() => window.clearInterval(interval));\n }\n\n return () => {\n for (const cleanup of cleanups) cleanup();\n };\n }\n\n return { subscribeLiveRefresh, hasPendingCrossAppSync };\n}\n","import type { Api } from '../api/create-api-client';\n\n/** Cross-app (suite) notification from `/api/notifications/` — shared across ShellStack apps. */\nexport interface SuiteNotification {\n id: string;\n title: string;\n body: string;\n href: string;\n kind: string;\n source_app: string;\n read_at: string | null;\n created_at: string;\n workspace: string | null;\n organisation: string | null;\n}\n\nexport interface SuiteNotificationsResponse {\n notifications: SuiteNotification[];\n unread_count: number;\n}\n\nexport interface NotificationsClientConfig {\n api: Api;\n parseApiError: (res: Response) => Promise<string>;\n cacheMs?: number;\n listPath?: string;\n}\n\nexport type NotificationsClient = ReturnType<typeof createNotificationsClient>;\n\nexport function createNotificationsClient(config: NotificationsClientConfig) {\n const { api, parseApiError } = config;\n const CACHE_MS = config.cacheMs ?? 15_000;\n const LIST_PATH = config.listPath ?? '/api/notifications/';\n\n let notificationsCache: {\n data: SuiteNotificationsResponse;\n at: number;\n } | null = null;\n let notificationsInflight: Promise<SuiteNotificationsResponse> | null = null;\n\n async function fetchSuiteNotifications(): Promise<SuiteNotificationsResponse> {\n if (notificationsCache && Date.now() - notificationsCache.at < CACHE_MS) {\n return notificationsCache.data;\n }\n if (notificationsInflight) return notificationsInflight;\n notificationsInflight = (async () => {\n try {\n const res = await api.get(LIST_PATH);\n if (!res.ok) throw new Error(await parseApiError(res));\n const data = (await res.json()) as Partial<SuiteNotificationsResponse>;\n const result: SuiteNotificationsResponse = {\n notifications: data.notifications ?? [],\n unread_count: data.unread_count ?? 0,\n };\n notificationsCache = { data: result, at: Date.now() };\n return result;\n } finally {\n notificationsInflight = null;\n }\n })();\n return notificationsInflight;\n }\n\n async function markSuiteNotificationRead(id: string): Promise<void> {\n const res = await api.post(`${LIST_PATH}${id}/read/`);\n if (!res.ok) throw new Error(await parseApiError(res));\n notificationsCache = null;\n }\n\n async function markAllSuiteNotificationsRead(): Promise<void> {\n const res = await api.post(`${LIST_PATH}read-all/`);\n if (!res.ok) throw new Error(await parseApiError(res));\n notificationsCache = null;\n }\n\n return {\n fetchSuiteNotifications,\n markSuiteNotificationRead,\n markAllSuiteNotificationsRead,\n };\n}\n","export type SuiteAiJobEvent = {\n sequence: number;\n event_type:\n | 'queued'\n | 'started'\n | 'progress'\n | 'tool'\n | 'completed'\n | 'failed'\n | 'cancelled';\n summary: string;\n payload: Record<string, unknown>;\n created_at: string;\n};\n\nexport type SuiteAiJob = {\n id: string;\n status: 'pending' | 'running' | 'completed' | 'failed';\n events: SuiteAiJobEvent[];\n};\n\nexport function activeSuiteAiJobs(jobs: SuiteAiJob[]) {\n return jobs.filter(\n (job) => job.status === 'pending' || job.status === 'running',\n );\n}\n\nexport function sortedSuiteAiEvents(events: SuiteAiJobEvent[]) {\n return [...events].sort((a, b) => a.sequence - b.sequence);\n}\n\nexport function suiteAiEventSummary(event?: SuiteAiJobEvent) {\n const summary = event?.summary\n // eslint-disable-next-line no-control-regex\n .replace(/[\\u0000-\\u001f]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n if (summary) return summary;\n if (event?.event_type === 'queued') return 'Shelly request queued';\n if (event?.event_type === 'started')\n return 'Shelly started preparing a reply';\n if (event?.event_type === 'completed') return 'Shelly reply ready';\n if (event?.event_type === 'failed') return 'AI request failed';\n return 'Shelly is working';\n}\n","export const WEBHOOK_ACTIVITY_ACTIONS = [\n { id: 'created', label: 'Task created' },\n { id: 'updated', label: 'Task updated' },\n { id: 'status_changed', label: 'Task status changed' },\n { id: 'assigned', label: 'Task assigned' },\n { id: 'commented', label: 'Comment posted' },\n { id: 'project_renamed', label: 'Project renamed' },\n { id: 'project_status_changed', label: 'Project archived or restored' },\n] as const;\n\nconst ALL_WEBHOOK_ACTION_IDS = WEBHOOK_ACTIVITY_ACTIONS.map(\n (action) => action.id,\n);\n\nexport function webhookActionsToSelected(\n stored: string[] | null | undefined,\n): Set<string> {\n if (!stored || stored.length === 0) {\n return new Set(ALL_WEBHOOK_ACTION_IDS);\n }\n return new Set(stored);\n}\n\nexport function selectedToWebhookActions(selected: Set<string>): string[] {\n if (selected.size >= ALL_WEBHOOK_ACTION_IDS.length) {\n return [];\n }\n return ALL_WEBHOOK_ACTION_IDS.filter((id) => selected.has(id));\n}\n","export interface JsonLdConfig {\n name: string;\n url: string;\n description: string;\n applicationCategory?: string;\n applicationSubCategory?: string;\n operatingSystem?: string;\n softwareVersion?: string;\n price?: string;\n priceCurrency?: string;\n inLanguage?: string;\n publisherName?: string;\n publisherUrl?: string;\n}\n\n/** Build a schema.org SoftwareApplication JSON-LD string for a product page. */\nexport function createJsonLd(config: JsonLdConfig): string {\n const publisherName = config.publisherName ?? 'Xenide';\n const publisherUrl = config.publisherUrl ?? 'https://xenide.io';\n\n return JSON.stringify({\n '@context': 'https://schema.org',\n '@type': 'SoftwareApplication',\n name: config.name,\n url: config.url,\n description: config.description,\n applicationCategory: config.applicationCategory ?? 'BusinessApplication',\n ...(config.applicationSubCategory\n ? { applicationSubCategory: config.applicationSubCategory }\n : {}),\n operatingSystem: config.operatingSystem ?? 'Web',\n browserRequirements: 'Requires JavaScript. Requires HTML5.',\n softwareVersion: config.softwareVersion ?? '1.0',\n offers: {\n '@type': 'Offer',\n price: config.price ?? '0',\n priceCurrency: config.priceCurrency ?? 'AUD',\n },\n publisher: {\n '@type': 'Organization',\n name: publisherName,\n url: publisherUrl,\n },\n author: {\n '@type': 'Organization',\n name: publisherName,\n url: publisherUrl,\n },\n inLanguage: config.inLanguage ?? 'en-AU',\n });\n}\n","'use client';\n\nimport { useEffect, useState } from 'react';\n\nconst NIGHT_START_HOUR = 18;\nconst NIGHT_END_HOUR = 6;\n\nexport function useIsNight(): boolean {\n const [isNight, setIsNight] = useState(false);\n\n useEffect(() => {\n const check = () => {\n const hour = new Date().getHours();\n return hour >= NIGHT_START_HOUR || hour < NIGHT_END_HOUR;\n };\n setIsNight(check());\n\n let timeout: ReturnType<typeof setTimeout>;\n const update = () => {\n const next = check();\n setIsNight((current) => (current !== next ? next : current));\n timeout = setTimeout(update, 60_000);\n };\n timeout = setTimeout(update, 60_000);\n return () => clearTimeout(timeout);\n }, []);\n\n return isNight;\n}\n"],"mappings":"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","names":["res","data"]}
|
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
|
-
|
|
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,
|
|
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
|
-
...
|
|
307
|
+
...requestInit.headers
|
|
297
308
|
};
|
|
298
|
-
const 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, { ...
|
|
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
|
-
|
|
330
|
-
|
|
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,
|
|
342
|
-
...
|
|
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
|
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors/codes.ts","../src/errors/normalize.ts","../src/api/create-api-client.ts","../src/auth/create-auth-return-path.ts","../src/workspace/access.ts","../src/workspace/subscription.ts","../src/workspace/create-brand-settings.ts","../src/workspace/create-workspace-appearance.ts","../src/workspace/create-workspace-appearance-api.ts","../src/sync/create-cross-app-sync.ts","../src/sync/create-live-refresh.ts","../src/notifications/create-notifications-client.ts","../src/ai/suite-ai-job.ts","../src/webhooks/webhook-activity-actions.ts","../src/seo/create-json-ld.ts","../src/hooks/use-is-night.ts"],"sourcesContent":["// AUTO-GENERATED from ERROR_CODES.md by scripts/error-codes/generate.py. Do not edit.\n\nexport const ERROR_CODES = {\n AUTH_001: 'E-AUTH-001',\n AUTH_002: 'E-AUTH-002',\n AUTH_003: 'E-AUTH-003',\n VALID_001: 'E-VALID-001',\n DATA_001: 'E-DATA-001',\n DATA_002: 'E-DATA-002',\n LIMIT_001: 'E-LIMIT-001',\n NET_001: 'E-NET-001',\n EXT_001: 'E-EXT-001',\n EXT_002: 'E-EXT-002',\n SYS_001: 'E-SYS-001',\n SYS_002: 'E-SYS-002',\n CLIENT_001: 'E-CLIENT-001',\n CLIENT_002: 'E-CLIENT-002',\n} as const;\n\nexport type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];\n\nexport const DEFAULT_ERROR_CODE: ErrorCode = 'E-SYS-001';\n\nexport const ERROR_COPY: Record<ErrorCode, string> = {\n 'E-AUTH-001': 'Sign-in was not accepted. Check your details and try again.',\n 'E-AUTH-002': 'Your session has ended. Sign in again to continue.',\n 'E-AUTH-003': 'You do not have access to this. Ask an administrator for access.',\n 'E-VALID-001': 'Some details need correcting before this can be saved.',\n 'E-DATA-001': 'We could not find what you asked for. It may have been removed.',\n 'E-DATA-002': 'This conflicts with existing information. Review it and try again.',\n 'E-LIMIT-001': 'Too many requests right now. Wait a moment and try again.',\n 'E-NET-001': 'We could not reach the service. Try again in a moment.',\n 'E-EXT-001': 'The <capability> is unavailable right now. Try again shortly.',\n 'E-EXT-002': 'The <capability> could not complete that request. Try again, or adjust the input.',\n 'E-SYS-001': 'Something went wrong on our side. We have recorded it. Try again.',\n 'E-SYS-002': 'This feature is temporarily unavailable while we carry out maintenance.',\n 'E-CLIENT-001': 'You appear to be offline. Check your connection and try again.',\n 'E-CLIENT-002': 'We could not read the response. Reload the page and try again.',\n};\n\nexport const ERROR_RETRYABLE: Record<ErrorCode, boolean> = {\n 'E-AUTH-001': false,\n 'E-AUTH-002': true,\n 'E-AUTH-003': false,\n 'E-VALID-001': false,\n 'E-DATA-001': false,\n 'E-DATA-002': false,\n 'E-LIMIT-001': true,\n 'E-NET-001': true,\n 'E-EXT-001': true,\n 'E-EXT-002': true,\n 'E-SYS-001': true,\n 'E-SYS-002': false,\n 'E-CLIENT-001': true,\n 'E-CLIENT-002': true,\n};\n\nconst ERROR_CODE_SET = new Set<string>(Object.values(ERROR_CODES));\n\nexport function isErrorCode(value: unknown): value is ErrorCode {\n return typeof value === 'string' && ERROR_CODE_SET.has(value);\n}\n\nexport function copyFor(code: ErrorCode, capability?: string): string {\n const copy = ERROR_COPY[code] ?? ERROR_COPY[DEFAULT_ERROR_CODE];\n if (copy.includes('<capability>')) {\n return copy.replace('<capability>', capability ?? 'service');\n }\n return copy;\n}\n","import {\n DEFAULT_ERROR_CODE,\n ERROR_CODES,\n ERROR_RETRYABLE,\n copyFor,\n isErrorCode,\n type ErrorCode,\n} from './codes';\n\n/**\n * The single normalised shape every caught failure is reduced to before any\n * component sees it. `message` is always resolved from the local registry;\n * upstream text is never carried through.\n */\nexport type AppError = {\n code: ErrorCode;\n message: string;\n correlationId?: string;\n retryable: boolean;\n};\n\nconst STATUS_TO_CODE: Record<number, ErrorCode> = {\n 400: ERROR_CODES.VALID_001,\n 401: ERROR_CODES.AUTH_001,\n 403: ERROR_CODES.AUTH_003,\n 404: ERROR_CODES.DATA_001,\n 405: ERROR_CODES.SYS_001,\n 409: ERROR_CODES.DATA_002,\n 422: ERROR_CODES.VALID_001,\n 429: ERROR_CODES.LIMIT_001,\n 500: ERROR_CODES.SYS_001,\n 502: ERROR_CODES.EXT_002,\n 503: ERROR_CODES.NET_001,\n};\n\nexport function codeForStatus(status: number): ErrorCode {\n if (STATUS_TO_CODE[status]) return STATUS_TO_CODE[status];\n if (status >= 500) return ERROR_CODES.SYS_001;\n if (status >= 400) return ERROR_CODES.VALID_001;\n return ERROR_CODES.SYS_001;\n}\n\nfunction build(\n code: ErrorCode,\n correlationId?: string,\n retryable?: boolean,\n capability?: string,\n): AppError {\n return {\n code,\n message: copyFor(code, capability),\n ...(correlationId ? { correlationId } : {}),\n retryable: retryable ?? ERROR_RETRYABLE[code],\n };\n}\n\nfunction extractEnvelope(\n input: unknown,\n): { code: string; correlationId?: string; retryable?: boolean } | null {\n if (typeof input !== 'object' || input === null) return null;\n const error = (input as Record<string, unknown>).error;\n if (typeof error !== 'object' || error === null) return null;\n const record = error as Record<string, unknown>;\n return {\n code: typeof record.code === 'string' ? record.code : '',\n correlationId:\n typeof record.correlation_id === 'string'\n ? record.correlation_id\n : undefined,\n retryable:\n typeof record.retryable === 'boolean' ? record.retryable : undefined,\n };\n}\n\n/**\n * Reduce an unknown value to an `AppError`. Accepts only the envelope shape;\n * every other key on the input is discarded. Unknown or absent codes fall back.\n */\nexport function normalizeError(\n input: unknown,\n fallback: ErrorCode = DEFAULT_ERROR_CODE,\n capability?: string,\n): AppError {\n const envelope = extractEnvelope(input);\n if (!envelope) return build(fallback, undefined, undefined, capability);\n const resolved = isErrorCode(envelope.code) ? envelope.code : fallback;\n return build(\n resolved,\n envelope.correlationId,\n envelope.retryable,\n capability,\n );\n}\n\n/** Normalise a non-2xx `Response` without ever reading its raw message. */\nexport async function appErrorFromResponse(\n response: Response,\n capability?: string,\n): Promise<AppError> {\n const fallback = codeForStatus(response.status);\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = undefined;\n }\n return normalizeError(body, fallback, capability);\n}\n\n/** Normalise a thrown fetch failure: offline, aborted, or unreachable. */\nexport function normalizeThrown(input: unknown, capability?: string): AppError {\n if (typeof navigator !== 'undefined' && navigator.onLine === false) {\n return build(ERROR_CODES.CLIENT_001, undefined, undefined, capability);\n }\n if (input instanceof DOMException && input.name === 'AbortError') {\n return build(ERROR_CODES.NET_001, undefined, undefined, capability);\n }\n return build(ERROR_CODES.NET_001, undefined, undefined, capability);\n}\n\n/** Display string for a failure surface: capability copy plus the code. */\nexport function formatAppError(error: AppError): string {\n return `${error.message} (Reference: ${error.code})`;\n}\n","import { appErrorFromResponse, formatAppError } from '../errors/normalize';\n\nexport interface ApiClientConfig {\n /** localStorage key namespace, e.g. `tt` or `tides`. */\n storagePrefix: string;\n /** Backend refresh endpoint, e.g. `/api/turtletime/auth/refresh/`. */\n refreshPath: string;\n /** Overrides `NEXT_PUBLIC_API_URL` / `http://localhost:8000`. */\n baseUrl?: string;\n accessTokenKey?: string;\n refreshTokenKey?: string;\n workspaceKey?: string;\n /** Suite-wide workspace cookie name. */\n workspaceCookieName?: string;\n /** Overrides `NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN`. */\n workspaceCookieDomain?: string;\n /** Fired when the active workspace changes in this tab. */\n onWorkspaceChange?: (id: string | null) => void;\n /** sessionStorage key prefixes to clear on sign-out. */\n sessionStorageClearPrefixes?: string[];\n}\n\nexport type ApiFetch = (\n path: string,\n options?: RequestInit,\n) => Promise<Response>;\n\n/**\n * Build a browser API client scoped to one ShellStack app. Every app shares the\n * same fetch/refresh/workspace behaviour; only the storage namespace and\n * refresh path differ.\n */\nexport function createApiClient(config: ApiClientConfig) {\n const API_URL =\n config.baseUrl ??\n process.env.NEXT_PUBLIC_API_URL ??\n 'http://localhost:8000';\n\n // Suite-wide workspace selection. Cookies (unlike localStorage) are shared\n // across ports/subdomains, so every product app reads the same value. Set\n // NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN (e.g. \".xenide.io\") to share across\n // subdomains in production; leave unset for localhost.\n const SHARED_WORKSPACE_COOKIE =\n config.workspaceCookieName ?? 'xenide_active_workspace_id';\n const SHARED_WORKSPACE_COOKIE_DOMAIN =\n config.workspaceCookieDomain ??\n process.env.NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN;\n\n const ACCESS_KEY = config.accessTokenKey ?? `${config.storagePrefix}_access_token`;\n const REFRESH_KEY =\n config.refreshTokenKey ?? `${config.storagePrefix}_refresh_token`;\n const WORKSPACE_KEY =\n config.workspaceKey ?? `${config.storagePrefix}_active_workspace_id`;\n\n let accessToken: string | null = null;\n let refreshToken: string | null = null;\n let activeWorkspaceId: string | null = null;\n\n let authFailureHandler: (() => void) | null = null;\n let authFailureFired = false;\n let refreshPromise: Promise<boolean> | null = null;\n\n function readSharedWorkspaceId(): string | null {\n if (typeof document === 'undefined') return null;\n const prefix = `${SHARED_WORKSPACE_COOKIE}=`;\n const entry = document.cookie\n .split('; ')\n .find((item) => item.startsWith(prefix));\n if (!entry) return null;\n const raw = entry.slice(prefix.length);\n if (!raw) return null;\n try {\n return decodeURIComponent(raw) || null;\n } catch {\n return raw;\n }\n }\n\n function writeSharedWorkspaceId(id: string | null): void {\n if (typeof document === 'undefined') return;\n const value = id ? encodeURIComponent(id) : '';\n const maxAge = id ? 60 * 60 * 24 * 365 : 0;\n const domain = SHARED_WORKSPACE_COOKIE_DOMAIN\n ? `; domain=${SHARED_WORKSPACE_COOKIE_DOMAIN}`\n : '';\n document.cookie = `${SHARED_WORKSPACE_COOKIE}=${value}; path=/; max-age=${maxAge}; SameSite=Lax${domain}`;\n }\n\n function onAuthFailure(handler: () => void): void {\n authFailureHandler = handler;\n authFailureFired = false;\n }\n\n function triggerAuthFailure(): void {\n if (authFailureFired) return;\n authFailureFired = true;\n authFailureHandler?.();\n }\n\n function setTokens(access: string, refresh: string): void {\n accessToken = access;\n refreshToken = refresh;\n if (typeof window !== 'undefined') {\n window.localStorage.setItem(ACCESS_KEY, access);\n window.localStorage.setItem(REFRESH_KEY, refresh);\n }\n }\n\n function getAccessToken(): string | null {\n if (accessToken) return accessToken;\n if (typeof window !== 'undefined') {\n accessToken = window.localStorage.getItem(ACCESS_KEY);\n }\n return accessToken;\n }\n\n function getRefreshToken(): string | null {\n if (refreshToken) return refreshToken;\n if (typeof window !== 'undefined') {\n refreshToken = window.localStorage.getItem(REFRESH_KEY);\n }\n return refreshToken;\n }\n\n function setActiveWorkspaceId(\n id: string | null,\n options?: { share?: boolean },\n ): void {\n const changed = activeWorkspaceId !== id;\n activeWorkspaceId = id;\n if (typeof window !== 'undefined') {\n if (id) {\n window.localStorage.setItem(WORKSPACE_KEY, id);\n } else {\n window.localStorage.removeItem(WORKSPACE_KEY);\n }\n if (changed) config.onWorkspaceChange?.(id);\n }\n // Only an explicit choice should update the suite-wide selection; a fallback\n // default must not clobber the workspace another app is using.\n if (options?.share) writeSharedWorkspaceId(id);\n }\n\n function getActiveWorkspaceId(): string | null {\n if (activeWorkspaceId) return activeWorkspaceId;\n if (typeof window !== 'undefined') {\n activeWorkspaceId =\n readSharedWorkspaceId() ?? window.localStorage.getItem(WORKSPACE_KEY);\n }\n return activeWorkspaceId;\n }\n\n function restoreWorkspace(): void {\n if (typeof window !== 'undefined') {\n activeWorkspaceId =\n readSharedWorkspaceId() ?? window.localStorage.getItem(WORKSPACE_KEY);\n }\n }\n\n function clearAuth(): void {\n accessToken = null;\n refreshToken = null;\n activeWorkspaceId = null;\n if (typeof window !== 'undefined') {\n window.localStorage.removeItem(ACCESS_KEY);\n window.localStorage.removeItem(REFRESH_KEY);\n window.localStorage.removeItem(WORKSPACE_KEY);\n const prefixes = config.sessionStorageClearPrefixes ?? [];\n if (prefixes.length > 0) {\n try {\n for (let i = window.sessionStorage.length - 1; i >= 0; i -= 1) {\n const key = window.sessionStorage.key(i);\n if (key && prefixes.some((prefix) => key.startsWith(prefix))) {\n window.sessionStorage.removeItem(key);\n }\n }\n } catch {\n // ignore storage access errors\n }\n }\n }\n }\n\n function restoreTokens(): void {\n if (typeof window !== 'undefined') {\n accessToken = window.localStorage.getItem(ACCESS_KEY);\n refreshToken = window.localStorage.getItem(REFRESH_KEY);\n activeWorkspaceId =\n readSharedWorkspaceId() ?? window.localStorage.getItem(WORKSPACE_KEY);\n }\n }\n\n async function refreshAccessToken(): Promise<boolean> {\n if (refreshPromise) return refreshPromise;\n refreshPromise = refreshAccessTokenInternal().finally(() => {\n refreshPromise = null;\n });\n return refreshPromise;\n }\n\n async function refreshAccessTokenInternal(): Promise<boolean> {\n const refresh = getRefreshToken();\n if (!refresh) {\n triggerAuthFailure();\n return false;\n }\n\n try {\n const res = await fetch(`${API_URL}${config.refreshPath}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({ refresh }),\n });\n if (!res.ok) {\n clearAuth();\n triggerAuthFailure();\n return false;\n }\n const data = await res.json();\n setTokens(data.access, data.refresh);\n authFailureFired = false;\n return true;\n } catch {\n clearAuth();\n triggerAuthFailure();\n return false;\n }\n }\n\n async function apiFetch(\n path: string,\n options: RequestInit = {},\n ): Promise<Response> {\n const token = getAccessToken();\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(options.headers as Record<string, string>),\n };\n\n const body = options.body;\n const isFormData =\n typeof FormData !== 'undefined' && body instanceof FormData;\n\n if (!isFormData) {\n headers['Content-Type'] = 'application/json';\n }\n\n if (token) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n const wsId = getActiveWorkspaceId();\n if (wsId) {\n headers['X-Active-Workspace-Id'] = wsId;\n }\n\n const url = path.startsWith('http') ? path : `${API_URL}${path}`;\n\n async function doFetch(): Promise<Response> {\n try {\n return await fetch(url, { ...options, headers });\n } catch {\n throw new Error(\n `Could not reach the API at ${API_URL}. Is the Django backend running?`,\n );\n }\n }\n\n let res = await doFetch();\n\n if (res.status === 401 && token) {\n const refreshed = await refreshAccessToken();\n if (refreshed) {\n const newToken = getAccessToken();\n if (newToken) {\n headers['Authorization'] = `Bearer ${newToken}`;\n }\n res = await doFetch();\n if (res.status === 401) {\n triggerAuthFailure();\n }\n }\n }\n\n return res;\n }\n\n async function parseApiError(res: Response): Promise<string> {\n // Never render upstream text or a status number: normalise, then show the code.\n return formatAppError(await appErrorFromResponse(res));\n }\n\n const api = {\n get: (path: string) => apiFetch(path),\n\n post: (path: string, body?: unknown, options: RequestInit = {}) =>\n apiFetch(path, {\n ...options,\n method: 'POST',\n body:\n body instanceof FormData\n ? body\n : body\n ? JSON.stringify(body)\n : undefined,\n }),\n\n put: (path: string, body?: unknown) =>\n apiFetch(path, {\n method: 'PUT',\n body:\n body instanceof FormData\n ? body\n : body\n ? JSON.stringify(body)\n : undefined,\n }),\n\n patch: (path: string, body?: unknown) =>\n apiFetch(path, {\n method: 'PATCH',\n body:\n body instanceof FormData\n ? body\n : body\n ? JSON.stringify(body)\n : undefined,\n }),\n\n delete: (path: string) => apiFetch(path, { method: 'DELETE' }),\n };\n\n return {\n API_URL,\n onAuthFailure,\n setTokens,\n getAccessToken,\n getRefreshToken,\n setActiveWorkspaceId,\n getActiveWorkspaceId,\n restoreWorkspace,\n clearAuth,\n restoreTokens,\n refreshAccessToken,\n parseApiError,\n api,\n };\n}\n\nexport type ApiClient = ReturnType<typeof createApiClient>;\nexport type Api = ApiClient['api'];\n","export interface AuthReturnPathConfig {\n /** sessionStorage key holding the in-app return path, e.g. `tt_auth_return_to`. */\n returnKey: string;\n /** sessionStorage key caching the portal handoff payload. */\n handoffKey?: string;\n defaultNext?: string;\n portalCallbackPath?: string;\n redeemPath?: string;\n}\n\nexport interface PortalHandoffPayload {\n code?: string;\n token?: string;\n access?: string;\n refresh?: string;\n}\n\nexport interface PortalHandoffRead {\n code: string | null;\n token: string | null;\n refresh: string | null;\n next: string;\n}\n\nexport type AuthReturnPath = ReturnType<typeof createAuthReturnPath>;\n\nexport function createAuthReturnPath(config: AuthReturnPathConfig) {\n const RETURN_KEY = config.returnKey;\n const HANDOFF_KEY = config.handoffKey ?? 'ss_portal_handoff';\n const DEFAULT_NEXT = config.defaultNext ?? '/today';\n const PORTAL_CALLBACK_PATH =\n config.portalCallbackPath ?? '/auth/portal/callback';\n const REDEEM_PATH = config.redeemPath ?? '/api/portal/auth/switch/redeem';\n\n function isSafeAuthReturnPath(\n path: string | null | undefined,\n ): path is string {\n if (\n !path ||\n path[0] !== '/' ||\n path.startsWith('//') ||\n path.startsWith('/\\\\')\n ) {\n return false;\n }\n if (path.includes('\\\\') || path.includes('://')) return false;\n return true;\n }\n\n function stashAuthReturnPath(path: string | null | undefined): void {\n if (typeof window === 'undefined' || !isSafeAuthReturnPath(path)) return;\n sessionStorage.setItem(RETURN_KEY, path);\n }\n\n function portalHandoffUrl(\n base: string,\n data: PortalHandoffPayload,\n next?: string,\n fallbackNext = DEFAULT_NEXT,\n ): string {\n const dest = isSafeAuthReturnPath(next) ? next : fallbackNext;\n const hash = new URLSearchParams();\n if (data.code) {\n hash.set('code', data.code);\n } else {\n const token = data.token || data.access || '';\n if (token) hash.set('token', token);\n if (data.refresh) hash.set('refresh', data.refresh);\n }\n hash.set('next', dest);\n return `${base.replace(/\\/$/, '')}${PORTAL_CALLBACK_PATH}#${hash.toString()}`;\n }\n\n function cachedHandoff(fallbackNext: string): PortalHandoffRead | null {\n try {\n const raw = sessionStorage.getItem(HANDOFF_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as PortalHandoffRead;\n if (!parsed.code && !parsed.token) return null;\n return {\n code: parsed.code || null,\n token: parsed.token || null,\n refresh: parsed.refresh || null,\n next: isSafeAuthReturnPath(parsed.next) ? parsed.next : fallbackNext,\n };\n } catch {\n sessionStorage.removeItem(HANDOFF_KEY);\n return null;\n }\n }\n\n function readPortalHandoff(fallbackNext = DEFAULT_NEXT): PortalHandoffRead {\n const cached = cachedHandoff(fallbackNext);\n if (cached) return cached;\n const hash = new URLSearchParams(window.location.hash.replace(/^#/, ''));\n const query = new URLSearchParams(window.location.search);\n const nextRaw = hash.get('next') || query.get('next');\n const result: PortalHandoffRead = {\n code: hash.get('code') || query.get('code'),\n token: hash.get('token') || query.get('token'),\n refresh: hash.get('refresh') || query.get('refresh'),\n next: isSafeAuthReturnPath(nextRaw) ? nextRaw : fallbackNext,\n };\n if (result.code || result.token) {\n sessionStorage.setItem(HANDOFF_KEY, JSON.stringify(result));\n }\n // The callback's hard location.replace removes the fragment from history.\n // Calling history.replaceState here makes Next.js remount the callback before\n // its one-time code redemption has completed.\n return result;\n }\n\n let redeemInflight: Promise<PortalHandoffRead> | null = null;\n\n async function redeemPortalHandoff(\n apiUrl: string,\n fallbackNext = DEFAULT_NEXT,\n ): Promise<PortalHandoffRead> {\n redeemInflight ??= (async () => {\n const { code, token, refresh, next } = readPortalHandoff(fallbackNext);\n if (!code) return { code: null, token, refresh, next };\n const response = await fetch(`${apiUrl.replace(/\\/$/, '')}${REDEEM_PATH}`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ code }),\n });\n if (!response.ok) return { code: null, token: null, refresh: null, next };\n const data = (await response.json()) as PortalHandoffPayload;\n sessionStorage.removeItem(HANDOFF_KEY);\n return {\n code: null,\n token: data.token || data.access || null,\n refresh: data.refresh || null,\n next,\n };\n })();\n return redeemInflight;\n }\n\n function readAuthReturnPath(): string | null {\n if (typeof window === 'undefined') return null;\n const path = sessionStorage.getItem(RETURN_KEY);\n return isSafeAuthReturnPath(path) ? path : null;\n }\n\n function signInHrefWithReturn(fallbackRedirect?: string | null): string {\n const path = isSafeAuthReturnPath(fallbackRedirect)\n ? fallbackRedirect\n : readAuthReturnPath();\n if (!path) return '/login';\n return `/login?redirect=${encodeURIComponent(path)}`;\n }\n\n function registerHrefWithReturn(returnPath: string): string {\n return `/register?redirect=${encodeURIComponent(returnPath)}`;\n }\n\n return {\n isSafeAuthReturnPath,\n stashAuthReturnPath,\n portalHandoffUrl,\n readPortalHandoff,\n redeemPortalHandoff,\n readAuthReturnPath,\n signInHrefWithReturn,\n registerHrefWithReturn,\n };\n}\n","/** Copy and helpers for workspace seats vs per-app access (shared core model). */\n\nexport const WORKSPACE_SEAT_HELP =\n 'Each member uses one workspace seat. Inviting someone reserves a seat until the invite is revoked or accepted.';\n\nexport const ORGANISATION_SEAT_HELP =\n 'Each organisation member uses one seat across all your workspaces. One invite gives them access to every workspace in your organisation.';\n\nexport const TIDES_ACCESS_HELP = 'Tides — project management.';\n\nexport const TURTLETIME_ACCESS_HELP =\n 'TurtleTime — time tracking add-on (Basic plan and above).';\n\nexport const FREE_PLAN_SEAT_HELP =\n 'Free personal workspaces include one product app only. Create an organisation on ShellStack for both apps and integration.';\n\nexport function hasAppAccess(\n canAccessTurtletime: boolean,\n canAccessTides: boolean,\n): boolean {\n return canAccessTurtletime || canAccessTides;\n}\n\nexport function canDisableAppAccess(\n app: 'turtletime' | 'tides',\n canAccessTurtletime: boolean,\n canAccessTides: boolean,\n): boolean {\n if (app === 'turtletime') return canAccessTides;\n return canAccessTurtletime;\n}\n","/** Per-workspace subscription tier helpers (mirrors core/subscriptions.py). */\n\nexport type SubscriptionTier =\n | 'free'\n | 'plus'\n | 'ai_plus'\n | 'basic'\n | 'pro'\n | 'enterprise';\n\nexport interface WorkspaceSubscription {\n tier: SubscriptionTier;\n tier_label: string;\n limits: {\n seats?: number | null;\n integrated_seats: number | null;\n tides_only_seats: number | null;\n };\n usage: {\n seats?: number;\n integrated_seats: number;\n tides_only_seats: number;\n };\n}\n\nexport type SeatType = 'tides' | 'integrated';\n\nconst TIER_LABELS: Record<SubscriptionTier, string> = {\n free: 'Free',\n plus: 'Plus',\n ai_plus: 'AI Plus',\n basic: 'Basic',\n pro: 'Pro',\n enterprise: 'Enterprise',\n};\n\nexport function tierLabel(tier: SubscriptionTier): string {\n if (tier === 'basic' || tier === 'pro') return 'Plus';\n return TIER_LABELS[tier] ?? 'Free';\n}\n\nexport function resolveSubscriptionTier(\n workspace:\n | {\n subscription?: { tier?: string } | null;\n subscription_tier?: string | null;\n }\n | null\n | undefined,\n): SubscriptionTier {\n const tier = workspace?.subscription?.tier ?? workspace?.subscription_tier;\n if (\n tier === 'plus' ||\n tier === 'ai_plus' ||\n tier === 'basic' ||\n tier === 'pro' ||\n tier === 'enterprise' ||\n tier === 'free'\n ) {\n return tier === 'basic' || tier === 'pro' ? 'plus' : tier;\n }\n return 'free';\n}\n\nexport function tierAllowsTurtletime(tier: SubscriptionTier): boolean {\n return tier !== 'free';\n}\n\nexport function defaultInviteSeatType(tier: SubscriptionTier): SeatType {\n return tierAllowsTurtletime(tier) ? 'integrated' : 'tides';\n}\n\nexport function tierAllowsTidesOnlySeats(): boolean {\n return false;\n}\n\nexport function seatTypeFromFlags(\n canAccessTurtletime: boolean,\n canAccessTides: boolean,\n): SeatType {\n if (canAccessTurtletime && canAccessTides) return 'integrated';\n return 'tides';\n}\n\nexport function flagsFromSeatType(seatType: SeatType): {\n can_access_turtletime: boolean;\n can_access_tides: boolean;\n} {\n if (seatType === 'integrated') {\n return { can_access_turtletime: true, can_access_tides: true };\n }\n return { can_access_turtletime: false, can_access_tides: true };\n}\n\nexport function seatTypeLabel(seatType: SeatType): string {\n return seatType === 'integrated' ? 'Tides + TurtleTime' : 'Tides';\n}\n\nexport function seatTypeDescription(\n tier: SubscriptionTier,\n seatType: SeatType,\n): string {\n if (seatType === 'integrated') {\n return 'Project management and time tracking with TT↔Tides integration.';\n }\n if (!tierAllowsTidesOnlySeats()) {\n return 'Project management included on the Free plan.';\n }\n return 'Project management only — no TurtleTime time tracking.';\n}\n\nexport function planSeatSummary(\n subscription: WorkspaceSubscription | null | undefined,\n): string {\n if (!subscription) return '';\n const { tier, limits, usage } = subscription;\n const seatCap = limits.seats ?? limits.integrated_seats;\n const seatCount = usage.seats ?? usage.integrated_seats;\n if (tier === 'enterprise') {\n return 'Unlimited seats';\n }\n if (seatCap != null) {\n return `${seatCount}/${seatCap} seats`;\n }\n return '';\n}\n","export interface BrandSettingsConfig {\n /** Fallback title when the workspace has not customised one. */\n defaultTitle: string;\n /** Window event dispatched when the brand changes. */\n changedEvent: string;\n}\n\nexport interface WorkspaceBrandPayload {\n brand_logo?: string | null;\n brand_title?: string | null;\n brand_show_title?: boolean;\n}\n\nexport type BrandSettings = ReturnType<typeof createBrandSettings>;\n\nexport function createBrandSettings(config: BrandSettingsConfig) {\n const DEFAULT_BRAND_TITLE = config.defaultTitle;\n const BRAND_SETTINGS_CHANGED = config.changedEvent;\n\n interface WorkspaceBrandState {\n logo: string | null;\n title: string | null;\n showTitle: boolean;\n }\n\n const brandState: WorkspaceBrandState = {\n logo: null,\n title: null,\n showTitle: true,\n };\n\n function notifyBrandChange(): void {\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new Event(BRAND_SETTINGS_CHANGED));\n }\n }\n\n function applyWorkspaceBrand(payload: WorkspaceBrandPayload): void {\n if ('brand_logo' in payload) {\n brandState.logo = payload.brand_logo ?? null;\n }\n if ('brand_title' in payload) {\n const title = payload.brand_title;\n brandState.title = title?.trim() ? title.trim() : null;\n }\n if ('brand_show_title' in payload && payload.brand_show_title !== undefined) {\n brandState.showTitle = payload.brand_show_title;\n }\n notifyBrandChange();\n }\n\n function getBrandLogo(): string | null {\n return brandState.logo;\n }\n\n function getCustomBrandTitle(): string | null {\n return brandState.title;\n }\n\n function getBrandShowTitle(): boolean {\n return brandState.showTitle;\n }\n\n /** Title shown in navigation — null when hidden. */\n function getDisplayBrandTitle(): string | null {\n if (!getBrandShowTitle()) return null;\n const custom = getCustomBrandTitle();\n if (custom === null) return DEFAULT_BRAND_TITLE;\n return custom || DEFAULT_BRAND_TITLE;\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function setBrandLogo(dataUrl: string | null): void {\n brandState.logo = dataUrl;\n notifyBrandChange();\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function setCustomBrandTitle(title: string | null): void {\n brandState.title = title?.trim() ? title.trim() : null;\n notifyBrandChange();\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function setBrandShowTitle(show: boolean): void {\n brandState.showTitle = show;\n notifyBrandChange();\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function resetBrandTitle(): void {\n brandState.title = null;\n brandState.showTitle = true;\n notifyBrandChange();\n }\n\n return {\n DEFAULT_BRAND_TITLE,\n BRAND_SETTINGS_CHANGED,\n /** @deprecated Use BRAND_SETTINGS_CHANGED */\n BRAND_LOGO_CHANGED: BRAND_SETTINGS_CHANGED,\n applyWorkspaceBrand,\n getBrandLogo,\n getCustomBrandTitle,\n getBrandShowTitle,\n getDisplayBrandTitle,\n setBrandLogo,\n setCustomBrandTitle,\n setBrandShowTitle,\n resetBrandTitle,\n };\n}\n","export interface WorkspaceAppearanceConfig {\n /** Window event dispatched when the active workspace changes. */\n changedEvent: string;\n /** localStorage base key for the theme. */\n themeStorageBase: string;\n /** Reads the active workspace id from the app's API client. */\n getActiveWorkspaceId: () => string | null;\n}\n\nexport type WorkspaceAppearanceHelpers = ReturnType<\n typeof createWorkspaceAppearance\n>;\n\nexport function createWorkspaceAppearance(config: WorkspaceAppearanceConfig) {\n const WORKSPACE_CHANGED = config.changedEvent;\n const THEME_STORAGE_BASE = config.themeStorageBase;\n\n function notifyWorkspaceChanged(): void {\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new Event(WORKSPACE_CHANGED));\n }\n }\n\n function scopedStorageKey(\n base: string,\n workspaceId?: string | null,\n ): string {\n const wsId = workspaceId ?? config.getActiveWorkspaceId();\n return wsId ? `${base}:${wsId}` : base;\n }\n\n function getScopedStorageItem(\n base: string,\n workspaceId?: string | null,\n ): string | null {\n if (typeof window === 'undefined') return null;\n try {\n const scoped = localStorage.getItem(scopedStorageKey(base, workspaceId));\n if (scoped !== null) return scoped;\n const wsId = workspaceId ?? config.getActiveWorkspaceId();\n if (wsId) {\n return localStorage.getItem(base);\n }\n return null;\n } catch {\n return null;\n }\n }\n\n function setScopedStorageItem(\n base: string,\n value: string | null,\n workspaceId?: string | null,\n ): void {\n if (typeof window === 'undefined') return;\n const key = scopedStorageKey(base, workspaceId);\n if (value === null) {\n localStorage.removeItem(key);\n } else {\n localStorage.setItem(key, value);\n }\n }\n\n return {\n WORKSPACE_CHANGED,\n THEME_STORAGE_BASE,\n notifyWorkspaceChanged,\n scopedStorageKey,\n getScopedStorageItem,\n setScopedStorageItem,\n };\n}\n","import type { Api } from '../api/create-api-client';\nimport type { WorkspaceBrandPayload } from './create-brand-settings';\n\nexport interface WorkspaceAppearanceApiConfig {\n api: Api;\n parseApiError: (res: Response) => Promise<string>;\n /** Endpoint base, e.g. `/api/turtletime/workspaces/current/appearance/`. */\n basePath: string;\n applyWorkspaceBrand: (payload: WorkspaceBrandPayload) => void;\n}\n\nexport type WorkspaceTheme = 'light' | 'dark';\n\nexport interface WorkspaceAppearance {\n theme: WorkspaceTheme;\n brand_logo: string | null;\n brand_title: string | null;\n brand_show_title: boolean;\n}\n\nexport type WorkspaceAppearancePatch = Partial<\n Pick<WorkspaceAppearance, 'theme' | 'brand_title' | 'brand_show_title'>\n> & {\n brand_logo_file?: File;\n clear_brand_logo?: boolean;\n};\n\nexport class WorkspaceAppearanceError extends Error {\n status: number;\n\n constructor(message: string, status: number) {\n super(message);\n this.status = status;\n }\n}\n\nfunction normalizeTheme(value: unknown): WorkspaceTheme {\n return value === 'light' ? 'light' : 'dark';\n}\n\nfunction parseAppearancePayload(\n data: Record<string, unknown>,\n): WorkspaceAppearance {\n const raw = (data.appearance ?? data) as Record<string, unknown>;\n return {\n theme: normalizeTheme(raw.theme),\n brand_logo: (raw.brand_logo as string | null) ?? null,\n brand_title: (raw.brand_title as string | null) ?? null,\n brand_show_title: Boolean(raw.brand_show_title),\n };\n}\n\nexport function createWorkspaceAppearanceApi(\n config: WorkspaceAppearanceApiConfig,\n) {\n const { api, parseApiError, basePath, applyWorkspaceBrand } = config;\n\n async function fetchWorkspaceAppearance(): Promise<WorkspaceAppearance> {\n const res = await api.get(basePath);\n if (!res.ok) {\n throw new WorkspaceAppearanceError(await parseApiError(res), res.status);\n }\n const data = await res.json();\n return parseAppearancePayload(data);\n }\n\n async function updateWorkspaceAppearance(\n patch: WorkspaceAppearancePatch,\n ): Promise<WorkspaceAppearance> {\n const hasFileUpload = patch.brand_logo_file instanceof File;\n const shouldClearLogo = patch.clear_brand_logo === true;\n\n if (hasFileUpload || shouldClearLogo) {\n const formData = new FormData();\n if (patch.theme) formData.append('theme', patch.theme);\n if (patch.brand_title !== undefined) {\n formData.append('brand_title', patch.brand_title ?? '');\n }\n if (patch.brand_show_title !== undefined) {\n formData.append(\n 'brand_show_title',\n patch.brand_show_title ? 'true' : 'false',\n );\n }\n if (hasFileUpload && patch.brand_logo_file) {\n formData.append('brand_logo', patch.brand_logo_file);\n }\n if (shouldClearLogo) {\n formData.append('clear_brand_logo', 'true');\n }\n\n const res = await api.patch(basePath, formData);\n if (!res.ok) {\n throw new Error(await parseApiError(res));\n }\n const data = await res.json();\n return parseAppearancePayload(data);\n }\n\n const res = await api.patch(basePath, {\n theme: patch.theme,\n brand_title: patch.brand_title,\n brand_show_title: patch.brand_show_title,\n });\n if (!res.ok) {\n throw new Error(await parseApiError(res));\n }\n const data = await res.json();\n return parseAppearancePayload(data);\n }\n\n function cacheWorkspaceAppearanceLocally(\n appearance: WorkspaceAppearance,\n ): void {\n applyWorkspaceBrand({\n brand_logo: appearance.brand_logo,\n brand_title: appearance.brand_title,\n brand_show_title: appearance.brand_show_title,\n });\n }\n\n return {\n WorkspaceAppearanceError,\n fetchWorkspaceAppearance,\n updateWorkspaceAppearance,\n cacheWorkspaceAppearanceLocally,\n };\n}\n","import type { Api } from '../api/create-api-client';\n\nexport type CrossAppSyncTopic = 'time' | 'integration' | 'tasks';\n\nexport interface CrossAppSyncConfig {\n api: Api;\n /** Checkpoint endpoint, e.g. `/api/turtletime/sync-checkpoint/`. */\n checkpointPath: string;\n /** Poll interval while the tab is visible (ms). */\n pollMs?: number;\n}\n\ntype SyncCheckpoint = {\n tasks_synced_at: number;\n time_synced_at: number;\n integration_synced_at: number;\n};\n\nexport type CrossAppSync = ReturnType<typeof createCrossAppSync>;\n\n/**\n * Poll backend checkpoints so sibling apps refresh when data mutates\n * elsewhere. Same-tab instant updates stay the app's responsibility via its\n * own window events.\n */\nexport function createCrossAppSync(config: CrossAppSyncConfig) {\n const pollMs = config.pollMs ?? 5000;\n\n const lastKnown: Record<CrossAppSyncTopic, number> = {\n time: 0,\n integration: 0,\n tasks: 0,\n };\n\n const subscribers: Array<{\n topic?: CrossAppSyncTopic;\n callback: () => void;\n lastSync: number;\n }> = [];\n\n let pollInterval: number | null = null;\n let pollListeners = 0;\n\n async function fetchCheckpoint(): Promise<SyncCheckpoint | null> {\n try {\n const res = await config.api.get(config.checkpointPath);\n if (!res.ok) return null;\n return (await res.json()) as SyncCheckpoint;\n } catch {\n return null;\n }\n }\n\n function notify(topic: CrossAppSyncTopic): void {\n for (const sub of subscribers) {\n if (!sub.topic || sub.topic === topic) {\n if (lastKnown[topic] > sub.lastSync) {\n sub.lastSync = lastKnown[topic];\n sub.callback();\n }\n }\n }\n }\n\n function checkAndNotify(data: SyncCheckpoint): void {\n const tasks = data.tasks_synced_at || 0;\n const time = data.time_synced_at || 0;\n const integration = data.integration_synced_at || 0;\n\n if (tasks > lastKnown.tasks) {\n lastKnown.tasks = tasks;\n notify('tasks');\n }\n if (time > lastKnown.time) {\n lastKnown.time = time;\n notify('time');\n }\n if (integration > lastKnown.integration) {\n lastKnown.integration = integration;\n notify('integration');\n }\n }\n\n function startPolling(): void {\n if (pollInterval !== null || typeof window === 'undefined') return;\n\n void fetchCheckpoint().then((data) => data && checkAndNotify(data));\n pollInterval = window.setInterval(() => {\n if (document.visibilityState === 'visible') {\n void fetchCheckpoint().then((data) => data && checkAndNotify(data));\n }\n }, pollMs);\n }\n\n function stopPolling(): void {\n if (pollListeners > 0) return;\n if (pollInterval !== null) {\n window.clearInterval(pollInterval);\n pollInterval = null;\n }\n }\n\n function broadcastCrossAppSync(_topic: CrossAppSyncTopic): void {\n // Cross-subdomain sync is handled by backend checkpoints + polling.\n // Same-tab instant updates are dispatched via the app's own events.\n void _topic;\n }\n\n function subscribeCrossAppSync(\n onSync: () => void,\n topic?: CrossAppSyncTopic,\n ): () => void {\n if (typeof window === 'undefined') return () => {};\n\n const sub = {\n topic,\n callback: onSync,\n lastSync: lastKnown[topic || 'tasks'],\n };\n subscribers.push(sub);\n pollListeners += 1;\n startPolling();\n\n return () => {\n const index = subscribers.indexOf(sub);\n if (index >= 0) subscribers.splice(index, 1);\n pollListeners -= 1;\n if (pollListeners <= 0) stopPolling();\n };\n }\n\n function lastCrossAppSyncTime(topic?: CrossAppSyncTopic): number {\n return topic ? lastKnown[topic] : Math.max(...Object.values(lastKnown));\n }\n\n return {\n broadcastCrossAppSync,\n subscribeCrossAppSync,\n lastCrossAppSyncTime,\n };\n}\n","import type {\n CrossAppSync,\n CrossAppSyncTopic,\n} from './create-cross-app-sync';\n\nexport interface LiveRefreshOptions {\n /** Same-tab custom events (e.g. `tt-timer-mutated`). */\n localEvents?: string[];\n /** Gentle poll while the tab is visible (ms). */\n pollMs?: number;\n /** Limit cross-tab refreshes to one mutation topic. */\n crossAppTopic?: CrossAppSyncTopic;\n}\n\nexport type LiveRefresh = ReturnType<typeof createLiveRefresh>;\n\nexport function createLiveRefresh(sync: CrossAppSync) {\n /** True if a cross-tab sync for the given topic happened after `since`. */\n function hasPendingCrossAppSync(\n topic: CrossAppSyncTopic | undefined,\n since: number,\n ): boolean {\n return sync.lastCrossAppSyncTime(topic) > since;\n }\n\n /** Re-run `refresh` when sibling apps mutate data, on same-tab events, or on an interval. */\n function subscribeLiveRefresh(\n refresh: () => void,\n options: LiveRefreshOptions = {},\n ): () => void {\n if (typeof window === 'undefined') return () => {};\n\n const cleanups: Array<() => void> = [];\n let lastRefreshAt = Date.now();\n\n const trigger = () => {\n lastRefreshAt = Date.now();\n refresh();\n };\n\n for (const eventName of options.localEvents ?? []) {\n window.addEventListener(eventName, trigger);\n cleanups.push(() => window.removeEventListener(eventName, trigger));\n }\n\n cleanups.push(sync.subscribeCrossAppSync(trigger, options.crossAppTopic));\n\n const shouldRefreshOnFocus = () =>\n hasPendingCrossAppSync(options.crossAppTopic, lastRefreshAt);\n\n const onVisibility = () => {\n if (document.visibilityState === 'visible' && shouldRefreshOnFocus()) {\n trigger();\n }\n };\n document.addEventListener('visibilitychange', onVisibility);\n cleanups.push(() =>\n document.removeEventListener('visibilitychange', onVisibility),\n );\n\n const onFocus = () => {\n if (shouldRefreshOnFocus()) {\n trigger();\n }\n };\n window.addEventListener('focus', onFocus);\n cleanups.push(() => window.removeEventListener('focus', onFocus));\n\n if (options.pollMs && options.pollMs > 0) {\n const interval = window.setInterval(() => {\n if (document.visibilityState === 'visible') trigger();\n }, options.pollMs);\n cleanups.push(() => window.clearInterval(interval));\n }\n\n return () => {\n for (const cleanup of cleanups) cleanup();\n };\n }\n\n return { subscribeLiveRefresh, hasPendingCrossAppSync };\n}\n","import type { Api } from '../api/create-api-client';\n\n/** Cross-app (suite) notification from `/api/notifications/` — shared across ShellStack apps. */\nexport interface SuiteNotification {\n id: string;\n title: string;\n body: string;\n href: string;\n kind: string;\n source_app: string;\n read_at: string | null;\n created_at: string;\n workspace: string | null;\n organisation: string | null;\n}\n\nexport interface SuiteNotificationsResponse {\n notifications: SuiteNotification[];\n unread_count: number;\n}\n\nexport interface NotificationsClientConfig {\n api: Api;\n parseApiError: (res: Response) => Promise<string>;\n cacheMs?: number;\n listPath?: string;\n}\n\nexport type NotificationsClient = ReturnType<typeof createNotificationsClient>;\n\nexport function createNotificationsClient(config: NotificationsClientConfig) {\n const { api, parseApiError } = config;\n const CACHE_MS = config.cacheMs ?? 15_000;\n const LIST_PATH = config.listPath ?? '/api/notifications/';\n\n let notificationsCache: {\n data: SuiteNotificationsResponse;\n at: number;\n } | null = null;\n let notificationsInflight: Promise<SuiteNotificationsResponse> | null = null;\n\n async function fetchSuiteNotifications(): Promise<SuiteNotificationsResponse> {\n if (notificationsCache && Date.now() - notificationsCache.at < CACHE_MS) {\n return notificationsCache.data;\n }\n if (notificationsInflight) return notificationsInflight;\n notificationsInflight = (async () => {\n try {\n const res = await api.get(LIST_PATH);\n if (!res.ok) throw new Error(await parseApiError(res));\n const data = (await res.json()) as Partial<SuiteNotificationsResponse>;\n const result: SuiteNotificationsResponse = {\n notifications: data.notifications ?? [],\n unread_count: data.unread_count ?? 0,\n };\n notificationsCache = { data: result, at: Date.now() };\n return result;\n } finally {\n notificationsInflight = null;\n }\n })();\n return notificationsInflight;\n }\n\n async function markSuiteNotificationRead(id: string): Promise<void> {\n const res = await api.post(`${LIST_PATH}${id}/read/`);\n if (!res.ok) throw new Error(await parseApiError(res));\n notificationsCache = null;\n }\n\n async function markAllSuiteNotificationsRead(): Promise<void> {\n const res = await api.post(`${LIST_PATH}read-all/`);\n if (!res.ok) throw new Error(await parseApiError(res));\n notificationsCache = null;\n }\n\n return {\n fetchSuiteNotifications,\n markSuiteNotificationRead,\n markAllSuiteNotificationsRead,\n };\n}\n","export type SuiteAiJobEvent = {\n sequence: number;\n event_type:\n | 'queued'\n | 'started'\n | 'progress'\n | 'tool'\n | 'completed'\n | 'failed'\n | 'cancelled';\n summary: string;\n payload: Record<string, unknown>;\n created_at: string;\n};\n\nexport type SuiteAiJob = {\n id: string;\n status: 'pending' | 'running' | 'completed' | 'failed';\n events: SuiteAiJobEvent[];\n};\n\nexport function activeSuiteAiJobs(jobs: SuiteAiJob[]) {\n return jobs.filter(\n (job) => job.status === 'pending' || job.status === 'running',\n );\n}\n\nexport function sortedSuiteAiEvents(events: SuiteAiJobEvent[]) {\n return [...events].sort((a, b) => a.sequence - b.sequence);\n}\n\nexport function suiteAiEventSummary(event?: SuiteAiJobEvent) {\n const summary = event?.summary\n // eslint-disable-next-line no-control-regex\n .replace(/[\\u0000-\\u001f]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n if (summary) return summary;\n if (event?.event_type === 'queued') return 'Shelly request queued';\n if (event?.event_type === 'started')\n return 'Shelly started preparing a reply';\n if (event?.event_type === 'completed') return 'Shelly reply ready';\n if (event?.event_type === 'failed') return 'AI request failed';\n return 'Shelly is working';\n}\n","export const WEBHOOK_ACTIVITY_ACTIONS = [\n { id: 'created', label: 'Task created' },\n { id: 'updated', label: 'Task updated' },\n { id: 'status_changed', label: 'Task status changed' },\n { id: 'assigned', label: 'Task assigned' },\n { id: 'commented', label: 'Comment posted' },\n { id: 'project_renamed', label: 'Project renamed' },\n { id: 'project_status_changed', label: 'Project archived or restored' },\n] as const;\n\nconst ALL_WEBHOOK_ACTION_IDS = WEBHOOK_ACTIVITY_ACTIONS.map(\n (action) => action.id,\n);\n\nexport function webhookActionsToSelected(\n stored: string[] | null | undefined,\n): Set<string> {\n if (!stored || stored.length === 0) {\n return new Set(ALL_WEBHOOK_ACTION_IDS);\n }\n return new Set(stored);\n}\n\nexport function selectedToWebhookActions(selected: Set<string>): string[] {\n if (selected.size >= ALL_WEBHOOK_ACTION_IDS.length) {\n return [];\n }\n return ALL_WEBHOOK_ACTION_IDS.filter((id) => selected.has(id));\n}\n","export interface JsonLdConfig {\n name: string;\n url: string;\n description: string;\n applicationCategory?: string;\n applicationSubCategory?: string;\n operatingSystem?: string;\n softwareVersion?: string;\n price?: string;\n priceCurrency?: string;\n inLanguage?: string;\n publisherName?: string;\n publisherUrl?: string;\n}\n\n/** Build a schema.org SoftwareApplication JSON-LD string for a product page. */\nexport function createJsonLd(config: JsonLdConfig): string {\n const publisherName = config.publisherName ?? 'Xenide';\n const publisherUrl = config.publisherUrl ?? 'https://xenide.io';\n\n return JSON.stringify({\n '@context': 'https://schema.org',\n '@type': 'SoftwareApplication',\n name: config.name,\n url: config.url,\n description: config.description,\n applicationCategory: config.applicationCategory ?? 'BusinessApplication',\n ...(config.applicationSubCategory\n ? { applicationSubCategory: config.applicationSubCategory }\n : {}),\n operatingSystem: config.operatingSystem ?? 'Web',\n browserRequirements: 'Requires JavaScript. Requires HTML5.',\n softwareVersion: config.softwareVersion ?? '1.0',\n offers: {\n '@type': 'Offer',\n price: config.price ?? '0',\n priceCurrency: config.priceCurrency ?? 'AUD',\n },\n publisher: {\n '@type': 'Organization',\n name: publisherName,\n url: publisherUrl,\n },\n author: {\n '@type': 'Organization',\n name: publisherName,\n url: publisherUrl,\n },\n inLanguage: config.inLanguage ?? 'en-AU',\n });\n}\n","'use client';\n\nimport { useEffect, useState } from 'react';\n\nconst NIGHT_START_HOUR = 18;\nconst NIGHT_END_HOUR = 6;\n\nexport function useIsNight(): boolean {\n const [isNight, setIsNight] = useState(false);\n\n useEffect(() => {\n const check = () => {\n const hour = new Date().getHours();\n return hour >= NIGHT_START_HOUR || hour < NIGHT_END_HOUR;\n };\n setIsNight(check());\n\n let timeout: ReturnType<typeof setTimeout>;\n const update = () => {\n const next = check();\n setIsNight((current) => (current !== next ? next : current));\n timeout = setTimeout(update, 60_000);\n };\n timeout = setTimeout(update, 60_000);\n return () => clearTimeout(timeout);\n }, []);\n\n return isNight;\n}\n"],"mappings":"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","names":["res","data"]}
|
|
1
|
+
{"version":3,"sources":["../src/errors/codes.ts","../src/errors/normalize.ts","../src/api/create-api-client.ts","../src/auth/create-auth-return-path.ts","../src/workspace/access.ts","../src/workspace/subscription.ts","../src/workspace/create-brand-settings.ts","../src/workspace/create-workspace-appearance.ts","../src/workspace/create-workspace-appearance-api.ts","../src/sync/create-cross-app-sync.ts","../src/sync/create-live-refresh.ts","../src/notifications/create-notifications-client.ts","../src/ai/suite-ai-job.ts","../src/webhooks/webhook-activity-actions.ts","../src/seo/create-json-ld.ts","../src/hooks/use-is-night.ts"],"sourcesContent":["// AUTO-GENERATED from ERROR_CODES.md by scripts/error-codes/generate.py. Do not edit.\n\nexport const ERROR_CODES = {\n AUTH_001: 'E-AUTH-001',\n AUTH_002: 'E-AUTH-002',\n AUTH_003: 'E-AUTH-003',\n VALID_001: 'E-VALID-001',\n DATA_001: 'E-DATA-001',\n DATA_002: 'E-DATA-002',\n LIMIT_001: 'E-LIMIT-001',\n NET_001: 'E-NET-001',\n EXT_001: 'E-EXT-001',\n EXT_002: 'E-EXT-002',\n SYS_001: 'E-SYS-001',\n SYS_002: 'E-SYS-002',\n CLIENT_001: 'E-CLIENT-001',\n CLIENT_002: 'E-CLIENT-002',\n} as const;\n\nexport type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];\n\nexport const DEFAULT_ERROR_CODE: ErrorCode = 'E-SYS-001';\n\nexport const ERROR_COPY: Record<ErrorCode, string> = {\n 'E-AUTH-001': 'Sign-in was not accepted. Check your details and try again.',\n 'E-AUTH-002': 'Your session has ended. Sign in again to continue.',\n 'E-AUTH-003': 'You do not have access to this. Ask an administrator for access.',\n 'E-VALID-001': 'Some details need correcting before this can be saved.',\n 'E-DATA-001': 'We could not find what you asked for. It may have been removed.',\n 'E-DATA-002': 'This conflicts with existing information. Review it and try again.',\n 'E-LIMIT-001': 'Too many requests right now. Wait a moment and try again.',\n 'E-NET-001': 'We could not reach the service. Try again in a moment.',\n 'E-EXT-001': 'The <capability> is unavailable right now. Try again shortly.',\n 'E-EXT-002': 'The <capability> could not complete that request. Try again, or adjust the input.',\n 'E-SYS-001': 'Something went wrong on our side. We have recorded it. Try again.',\n 'E-SYS-002': 'This feature is temporarily unavailable while we carry out maintenance.',\n 'E-CLIENT-001': 'You appear to be offline. Check your connection and try again.',\n 'E-CLIENT-002': 'We could not read the response. Reload the page and try again.',\n};\n\nexport const ERROR_RETRYABLE: Record<ErrorCode, boolean> = {\n 'E-AUTH-001': false,\n 'E-AUTH-002': true,\n 'E-AUTH-003': false,\n 'E-VALID-001': false,\n 'E-DATA-001': false,\n 'E-DATA-002': false,\n 'E-LIMIT-001': true,\n 'E-NET-001': true,\n 'E-EXT-001': true,\n 'E-EXT-002': true,\n 'E-SYS-001': true,\n 'E-SYS-002': false,\n 'E-CLIENT-001': true,\n 'E-CLIENT-002': true,\n};\n\nconst ERROR_CODE_SET = new Set<string>(Object.values(ERROR_CODES));\n\nexport function isErrorCode(value: unknown): value is ErrorCode {\n return typeof value === 'string' && ERROR_CODE_SET.has(value);\n}\n\nexport function copyFor(code: ErrorCode, capability?: string): string {\n const copy = ERROR_COPY[code] ?? ERROR_COPY[DEFAULT_ERROR_CODE];\n if (copy.includes('<capability>')) {\n return copy.replace('<capability>', capability ?? 'service');\n }\n return copy;\n}\n","import {\n DEFAULT_ERROR_CODE,\n ERROR_CODES,\n ERROR_RETRYABLE,\n copyFor,\n isErrorCode,\n type ErrorCode,\n} from './codes';\n\n/**\n * The single normalised shape every caught failure is reduced to before any\n * component sees it. `message` is always resolved from the local registry;\n * upstream text is never carried through.\n */\nexport type AppError = {\n code: ErrorCode;\n message: string;\n correlationId?: string;\n retryable: boolean;\n};\n\nconst STATUS_TO_CODE: Record<number, ErrorCode> = {\n 400: ERROR_CODES.VALID_001,\n 401: ERROR_CODES.AUTH_001,\n 403: ERROR_CODES.AUTH_003,\n 404: ERROR_CODES.DATA_001,\n 405: ERROR_CODES.SYS_001,\n 409: ERROR_CODES.DATA_002,\n 422: ERROR_CODES.VALID_001,\n 429: ERROR_CODES.LIMIT_001,\n 500: ERROR_CODES.SYS_001,\n 502: ERROR_CODES.EXT_002,\n 503: ERROR_CODES.NET_001,\n};\n\nexport function codeForStatus(status: number): ErrorCode {\n if (STATUS_TO_CODE[status]) return STATUS_TO_CODE[status];\n if (status >= 500) return ERROR_CODES.SYS_001;\n if (status >= 400) return ERROR_CODES.VALID_001;\n return ERROR_CODES.SYS_001;\n}\n\nfunction build(\n code: ErrorCode,\n correlationId?: string,\n retryable?: boolean,\n capability?: string,\n): AppError {\n return {\n code,\n message: copyFor(code, capability),\n ...(correlationId ? { correlationId } : {}),\n retryable: retryable ?? ERROR_RETRYABLE[code],\n };\n}\n\nfunction extractEnvelope(\n input: unknown,\n): { code: string; correlationId?: string; retryable?: boolean } | null {\n if (typeof input !== 'object' || input === null) return null;\n const error = (input as Record<string, unknown>).error;\n if (typeof error !== 'object' || error === null) return null;\n const record = error as Record<string, unknown>;\n return {\n code: typeof record.code === 'string' ? record.code : '',\n correlationId:\n typeof record.correlation_id === 'string'\n ? record.correlation_id\n : undefined,\n retryable:\n typeof record.retryable === 'boolean' ? record.retryable : undefined,\n };\n}\n\n/**\n * Reduce an unknown value to an `AppError`. Accepts only the envelope shape;\n * every other key on the input is discarded. Unknown or absent codes fall back.\n */\nexport function normalizeError(\n input: unknown,\n fallback: ErrorCode = DEFAULT_ERROR_CODE,\n capability?: string,\n): AppError {\n const envelope = extractEnvelope(input);\n if (!envelope) return build(fallback, undefined, undefined, capability);\n const resolved = isErrorCode(envelope.code) ? envelope.code : fallback;\n return build(\n resolved,\n envelope.correlationId,\n envelope.retryable,\n capability,\n );\n}\n\n/** Normalise a non-2xx `Response` without ever reading its raw message. */\nexport async function appErrorFromResponse(\n response: Response,\n capability?: string,\n): Promise<AppError> {\n const fallback = codeForStatus(response.status);\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n body = undefined;\n }\n return normalizeError(body, fallback, capability);\n}\n\n/** Normalise a thrown fetch failure: offline, aborted, or unreachable. */\nexport function normalizeThrown(input: unknown, capability?: string): AppError {\n if (typeof navigator !== 'undefined' && navigator.onLine === false) {\n return build(ERROR_CODES.CLIENT_001, undefined, undefined, capability);\n }\n if (input instanceof DOMException && input.name === 'AbortError') {\n return build(ERROR_CODES.NET_001, undefined, undefined, capability);\n }\n return build(ERROR_CODES.NET_001, undefined, undefined, capability);\n}\n\n/** Display string for a failure surface: capability copy plus the code. */\nexport function formatAppError(error: AppError): string {\n return `${error.message} (Reference: ${error.code})`;\n}\n","import { appErrorFromResponse, formatAppError } from '../errors/normalize';\n\nexport type ApiFetchInit = RequestInit & { skipAuth?: boolean };\n\nexport interface ApiClientConfig {\n /** localStorage key namespace, e.g. `tt` or `tides`. */\n storagePrefix: string;\n /** Backend refresh endpoint, e.g. `/api/turtletime/auth/refresh/`. */\n refreshPath: string;\n /** Overrides `NEXT_PUBLIC_API_URL` / `http://localhost:8000`. */\n baseUrl?: string;\n accessTokenKey?: string;\n refreshTokenKey?: string;\n workspaceKey?: string;\n /** Suite-wide workspace cookie name. */\n workspaceCookieName?: string;\n /** Overrides `NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN`. */\n workspaceCookieDomain?: string;\n /** Fired when the active workspace changes in this tab. */\n onWorkspaceChange?: (id: string | null) => void;\n /** Fired during sign-out before storage is cleared. */\n onSignOut?: () => void;\n /** Clear the workspace key on sign-out (default `true`). */\n clearWorkspaceOnSignOut?: boolean;\n /** Extra localStorage keys removed on sign-out (e.g. portal app tokens). */\n extraStorageKeysOnClear?: string[];\n /** sessionStorage key prefixes to clear on sign-out. */\n sessionStorageClearPrefixes?: string[];\n /** Refuse to send the session to an unexpected absolute origin. */\n enforceSameOrigin?: boolean;\n}\n\n/**\n * Build a browser API client scoped to one ShellStack app. Every app shares the\n * same fetch/refresh/workspace behaviour; only the storage namespace and\n * refresh path differ.\n */\nexport function createApiClient(config: ApiClientConfig) {\n const API_URL =\n config.baseUrl ??\n process.env.NEXT_PUBLIC_API_URL ??\n 'http://localhost:8000';\n\n // Suite-wide workspace selection. Cookies (unlike localStorage) are shared\n // across ports/subdomains, so every product app reads the same value. Set\n // NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN (e.g. \".xenide.io\") to share across\n // subdomains in production; leave unset for localhost.\n const SHARED_WORKSPACE_COOKIE =\n config.workspaceCookieName ?? 'xenide_active_workspace_id';\n const SHARED_WORKSPACE_COOKIE_DOMAIN =\n config.workspaceCookieDomain ??\n process.env.NEXT_PUBLIC_WORKSPACE_COOKIE_DOMAIN;\n\n const ACCESS_KEY = config.accessTokenKey ?? `${config.storagePrefix}_access_token`;\n const REFRESH_KEY =\n config.refreshTokenKey ?? `${config.storagePrefix}_refresh_token`;\n const WORKSPACE_KEY =\n config.workspaceKey ?? `${config.storagePrefix}_active_workspace_id`;\n const CLEAR_WORKSPACE = config.clearWorkspaceOnSignOut ?? true;\n\n let accessToken: string | null = null;\n let refreshToken: string | null = null;\n let activeWorkspaceId: string | null = null;\n\n let authFailureHandler: (() => void) | null = null;\n let authFailureFired = false;\n let refreshPromise: Promise<boolean> | null = null;\n\n function readSharedWorkspaceId(): string | null {\n if (typeof document === 'undefined') return null;\n const prefix = `${SHARED_WORKSPACE_COOKIE}=`;\n const entry = document.cookie\n .split('; ')\n .find((item) => item.startsWith(prefix));\n if (!entry) return null;\n const raw = entry.slice(prefix.length);\n if (!raw) return null;\n try {\n return decodeURIComponent(raw) || null;\n } catch {\n return raw;\n }\n }\n\n function writeSharedWorkspaceId(id: string | null): void {\n if (typeof document === 'undefined') return;\n const value = id ? encodeURIComponent(id) : '';\n const maxAge = id ? 60 * 60 * 24 * 365 : 0;\n const domain = SHARED_WORKSPACE_COOKIE_DOMAIN\n ? `; domain=${SHARED_WORKSPACE_COOKIE_DOMAIN}`\n : '';\n document.cookie = `${SHARED_WORKSPACE_COOKIE}=${value}; path=/; max-age=${maxAge}; SameSite=Lax${domain}`;\n }\n\n function onAuthFailure(handler: () => void): void {\n authFailureHandler = handler;\n authFailureFired = false;\n }\n\n function triggerAuthFailure(): void {\n if (authFailureFired) return;\n authFailureFired = true;\n authFailureHandler?.();\n }\n\n function setTokens(access: string, refresh?: string | null): void {\n accessToken = access;\n authFailureFired = false;\n if (refresh !== undefined) refreshToken = refresh;\n if (typeof window !== 'undefined') {\n window.localStorage.setItem(ACCESS_KEY, access);\n if (refresh) window.localStorage.setItem(REFRESH_KEY, refresh);\n else if (refresh !== undefined) window.localStorage.removeItem(REFRESH_KEY);\n }\n }\n\n function getAccessToken(): string | null {\n if (accessToken) return accessToken;\n if (typeof window !== 'undefined') {\n accessToken = window.localStorage.getItem(ACCESS_KEY);\n }\n return accessToken;\n }\n\n function getRefreshToken(): string | null {\n if (refreshToken) return refreshToken;\n if (typeof window !== 'undefined') {\n refreshToken = window.localStorage.getItem(REFRESH_KEY);\n }\n return refreshToken;\n }\n\n function hasAccessToken(): boolean {\n return Boolean(getAccessToken());\n }\n\n function setActiveWorkspaceId(\n id: string | null,\n options?: { share?: boolean },\n ): void {\n const changed = activeWorkspaceId !== id;\n activeWorkspaceId = id;\n if (typeof window !== 'undefined') {\n if (id) {\n window.localStorage.setItem(WORKSPACE_KEY, id);\n } else {\n window.localStorage.removeItem(WORKSPACE_KEY);\n }\n if (changed) config.onWorkspaceChange?.(id);\n }\n // Only an explicit choice should update the suite-wide selection; a fallback\n // default must not clobber the workspace another app is using.\n if (options?.share) writeSharedWorkspaceId(id);\n }\n\n function getActiveWorkspaceId(): string | null {\n if (activeWorkspaceId) return activeWorkspaceId;\n if (typeof window !== 'undefined') {\n activeWorkspaceId =\n readSharedWorkspaceId() ?? window.localStorage.getItem(WORKSPACE_KEY);\n }\n return activeWorkspaceId;\n }\n\n function restoreWorkspace(): void {\n if (typeof window !== 'undefined') {\n activeWorkspaceId =\n readSharedWorkspaceId() ?? window.localStorage.getItem(WORKSPACE_KEY);\n }\n }\n\n function clearAuth(): void {\n config.onSignOut?.();\n accessToken = null;\n refreshToken = null;\n activeWorkspaceId = null;\n if (typeof window !== 'undefined') {\n window.localStorage.removeItem(ACCESS_KEY);\n window.localStorage.removeItem(REFRESH_KEY);\n if (CLEAR_WORKSPACE) window.localStorage.removeItem(WORKSPACE_KEY);\n for (const key of config.extraStorageKeysOnClear ?? []) {\n window.localStorage.removeItem(key);\n }\n const prefixes = config.sessionStorageClearPrefixes ?? [];\n if (prefixes.length > 0) {\n try {\n for (let i = window.sessionStorage.length - 1; i >= 0; i -= 1) {\n const key = window.sessionStorage.key(i);\n if (key && prefixes.some((prefix) => key.startsWith(prefix))) {\n window.sessionStorage.removeItem(key);\n }\n }\n } catch {\n // ignore storage access errors\n }\n }\n }\n }\n\n function restoreTokens(): void {\n if (typeof window !== 'undefined') {\n accessToken = window.localStorage.getItem(ACCESS_KEY);\n refreshToken = window.localStorage.getItem(REFRESH_KEY);\n activeWorkspaceId =\n readSharedWorkspaceId() ?? window.localStorage.getItem(WORKSPACE_KEY);\n }\n }\n\n async function refreshAccessToken(): Promise<boolean> {\n if (refreshPromise) return refreshPromise;\n refreshPromise = refreshAccessTokenInternal().finally(() => {\n refreshPromise = null;\n });\n return refreshPromise;\n }\n\n async function refreshAccessTokenInternal(): Promise<boolean> {\n const refresh = getRefreshToken();\n if (!refresh) {\n triggerAuthFailure();\n return false;\n }\n\n try {\n const res = await fetch(`${API_URL}${config.refreshPath}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({ refresh }),\n });\n if (!res.ok) {\n clearAuth();\n triggerAuthFailure();\n return false;\n }\n const data = await res.json();\n setTokens(data.access, data.refresh);\n authFailureFired = false;\n return true;\n } catch {\n clearAuth();\n triggerAuthFailure();\n return false;\n }\n }\n\n async function apiFetch(\n path: string,\n init: ApiFetchInit = {},\n ): Promise<Response> {\n const { skipAuth, ...requestInit } = init;\n const token = getAccessToken();\n const headers: Record<string, string> = {\n Accept: 'application/json',\n ...(requestInit.headers as Record<string, string>),\n };\n\n const body = requestInit.body;\n const isFormData =\n typeof FormData !== 'undefined' && body instanceof FormData;\n\n if (!isFormData) {\n headers['Content-Type'] = 'application/json';\n }\n\n if (token && !skipAuth) {\n headers['Authorization'] = `Bearer ${token}`;\n }\n\n const wsId = getActiveWorkspaceId();\n if (wsId && !skipAuth) {\n headers['X-Active-Workspace-Id'] = wsId;\n }\n\n const url = path.startsWith('http') ? path : `${API_URL}${path}`;\n if (\n config.enforceSameOrigin &&\n !skipAuth &&\n path.startsWith('http') &&\n !url.startsWith(API_URL)\n ) {\n throw new Error('Refusing to send the session to an unexpected origin.');\n }\n\n async function doFetch(): Promise<Response> {\n try {\n return await fetch(url, { ...requestInit, headers });\n } catch (error) {\n if (\n init.signal?.aborted ||\n (error instanceof DOMException && error.name === 'AbortError')\n ) {\n throw error;\n }\n throw new Error(\n `Could not reach the API at ${API_URL}. Is the Django backend running?`,\n );\n }\n }\n\n let res = await doFetch();\n\n if (res.status === 401 && token && !skipAuth) {\n const refreshed = await refreshAccessToken();\n if (refreshed) {\n const newToken = getAccessToken();\n if (newToken) {\n headers['Authorization'] = `Bearer ${newToken}`;\n }\n res = await doFetch();\n }\n if (res.status === 401) {\n clearAuth();\n triggerAuthFailure();\n }\n }\n\n return res;\n }\n\n async function parseApiError(res: Response): Promise<string> {\n // Never render upstream text or a status number: normalise, then show the code.\n return formatAppError(await appErrorFromResponse(res));\n }\n\n const api = {\n get: (path: string, init?: ApiFetchInit) => apiFetch(path, init),\n\n post: (path: string, body?: unknown, init: ApiFetchInit = {}) =>\n apiFetch(path, {\n ...init,\n method: 'POST',\n body:\n body instanceof FormData\n ? body\n : body\n ? JSON.stringify(body)\n : undefined,\n }),\n\n put: (path: string, body?: unknown, init: ApiFetchInit = {}) =>\n apiFetch(path, {\n ...init,\n method: 'PUT',\n body:\n body instanceof FormData\n ? body\n : body\n ? JSON.stringify(body)\n : undefined,\n }),\n\n patch: (path: string, body?: unknown, init: ApiFetchInit = {}) =>\n apiFetch(path, {\n ...init,\n method: 'PATCH',\n body:\n body instanceof FormData\n ? body\n : body\n ? JSON.stringify(body)\n : undefined,\n }),\n\n delete: (path: string, init: ApiFetchInit = {}) =>\n apiFetch(path, { ...init, method: 'DELETE' }),\n };\n\n return {\n API_URL,\n onAuthFailure,\n setTokens,\n getAccessToken,\n getRefreshToken,\n hasAccessToken,\n setActiveWorkspaceId,\n getActiveWorkspaceId,\n restoreWorkspace,\n clearAuth,\n restoreTokens,\n refreshAccessToken,\n parseApiError,\n api,\n // Aliases used by Kraken/Shelly.\n restoreSession: restoreTokens,\n clearSession: clearAuth,\n };\n}\n\nexport type ApiClient = ReturnType<typeof createApiClient>;\nexport type Api = ApiClient['api'];\n","export interface AuthReturnPathConfig {\n /** sessionStorage key holding the in-app return path, e.g. `tt_auth_return_to`. */\n returnKey: string;\n /** sessionStorage key caching the portal handoff payload. */\n handoffKey?: string;\n defaultNext?: string;\n portalCallbackPath?: string;\n redeemPath?: string;\n}\n\nexport interface PortalHandoffPayload {\n code?: string;\n token?: string;\n access?: string;\n refresh?: string;\n}\n\nexport interface PortalHandoffRead {\n code: string | null;\n token: string | null;\n refresh: string | null;\n next: string;\n}\n\nexport type AuthReturnPath = ReturnType<typeof createAuthReturnPath>;\n\nexport function createAuthReturnPath(config: AuthReturnPathConfig) {\n const RETURN_KEY = config.returnKey;\n const HANDOFF_KEY = config.handoffKey ?? 'ss_portal_handoff';\n const DEFAULT_NEXT = config.defaultNext ?? '/today';\n const PORTAL_CALLBACK_PATH =\n config.portalCallbackPath ?? '/auth/portal/callback';\n const REDEEM_PATH = config.redeemPath ?? '/api/portal/auth/switch/redeem';\n\n function isSafeAuthReturnPath(\n path: string | null | undefined,\n ): path is string {\n if (\n !path ||\n path[0] !== '/' ||\n path.startsWith('//') ||\n path.startsWith('/\\\\')\n ) {\n return false;\n }\n if (path.includes('\\\\') || path.includes('://')) return false;\n return true;\n }\n\n function stashAuthReturnPath(path: string | null | undefined): void {\n if (typeof window === 'undefined' || !isSafeAuthReturnPath(path)) return;\n sessionStorage.setItem(RETURN_KEY, path);\n }\n\n function portalHandoffUrl(\n base: string,\n data: PortalHandoffPayload,\n next?: string,\n fallbackNext = DEFAULT_NEXT,\n ): string {\n const dest = isSafeAuthReturnPath(next) ? next : fallbackNext;\n const hash = new URLSearchParams();\n if (data.code) {\n hash.set('code', data.code);\n } else {\n const token = data.token || data.access || '';\n if (token) hash.set('token', token);\n if (data.refresh) hash.set('refresh', data.refresh);\n }\n hash.set('next', dest);\n return `${base.replace(/\\/$/, '')}${PORTAL_CALLBACK_PATH}#${hash.toString()}`;\n }\n\n function cachedHandoff(fallbackNext: string): PortalHandoffRead | null {\n try {\n const raw = sessionStorage.getItem(HANDOFF_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as PortalHandoffRead;\n if (!parsed.code && !parsed.token) return null;\n return {\n code: parsed.code || null,\n token: parsed.token || null,\n refresh: parsed.refresh || null,\n next: isSafeAuthReturnPath(parsed.next) ? parsed.next : fallbackNext,\n };\n } catch {\n sessionStorage.removeItem(HANDOFF_KEY);\n return null;\n }\n }\n\n function readPortalHandoff(fallbackNext = DEFAULT_NEXT): PortalHandoffRead {\n const cached = cachedHandoff(fallbackNext);\n if (cached) return cached;\n const hash = new URLSearchParams(window.location.hash.replace(/^#/, ''));\n const query = new URLSearchParams(window.location.search);\n const nextRaw = hash.get('next') || query.get('next');\n const result: PortalHandoffRead = {\n code: hash.get('code') || query.get('code'),\n token: hash.get('token') || query.get('token'),\n refresh: hash.get('refresh') || query.get('refresh'),\n next: isSafeAuthReturnPath(nextRaw) ? nextRaw : fallbackNext,\n };\n if (result.code || result.token) {\n sessionStorage.setItem(HANDOFF_KEY, JSON.stringify(result));\n }\n // The callback's hard location.replace removes the fragment from history.\n // Calling history.replaceState here makes Next.js remount the callback before\n // its one-time code redemption has completed.\n return result;\n }\n\n let redeemInflight: Promise<PortalHandoffRead> | null = null;\n\n async function redeemPortalHandoff(\n apiUrl: string,\n fallbackNext = DEFAULT_NEXT,\n ): Promise<PortalHandoffRead> {\n redeemInflight ??= (async () => {\n const { code, token, refresh, next } = readPortalHandoff(fallbackNext);\n if (!code) return { code: null, token, refresh, next };\n const response = await fetch(`${apiUrl.replace(/\\/$/, '')}${REDEEM_PATH}`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ code }),\n });\n if (!response.ok) return { code: null, token: null, refresh: null, next };\n const data = (await response.json()) as PortalHandoffPayload;\n sessionStorage.removeItem(HANDOFF_KEY);\n return {\n code: null,\n token: data.token || data.access || null,\n refresh: data.refresh || null,\n next,\n };\n })();\n return redeemInflight;\n }\n\n function readAuthReturnPath(): string | null {\n if (typeof window === 'undefined') return null;\n const path = sessionStorage.getItem(RETURN_KEY);\n return isSafeAuthReturnPath(path) ? path : null;\n }\n\n function signInHrefWithReturn(fallbackRedirect?: string | null): string {\n const path = isSafeAuthReturnPath(fallbackRedirect)\n ? fallbackRedirect\n : readAuthReturnPath();\n if (!path) return '/login';\n return `/login?redirect=${encodeURIComponent(path)}`;\n }\n\n function registerHrefWithReturn(returnPath: string): string {\n return `/register?redirect=${encodeURIComponent(returnPath)}`;\n }\n\n return {\n isSafeAuthReturnPath,\n stashAuthReturnPath,\n portalHandoffUrl,\n readPortalHandoff,\n redeemPortalHandoff,\n readAuthReturnPath,\n signInHrefWithReturn,\n registerHrefWithReturn,\n };\n}\n","/** Copy and helpers for workspace seats vs per-app access (shared core model). */\n\nexport const WORKSPACE_SEAT_HELP =\n 'Each member uses one workspace seat. Inviting someone reserves a seat until the invite is revoked or accepted.';\n\nexport const ORGANISATION_SEAT_HELP =\n 'Each organisation member uses one seat across all your workspaces. One invite gives them access to every workspace in your organisation.';\n\nexport const TIDES_ACCESS_HELP = 'Tides — project management.';\n\nexport const TURTLETIME_ACCESS_HELP =\n 'TurtleTime — time tracking add-on (Basic plan and above).';\n\nexport const FREE_PLAN_SEAT_HELP =\n 'Free personal workspaces include one product app only. Create an organisation on ShellStack for both apps and integration.';\n\nexport function hasAppAccess(\n canAccessTurtletime: boolean,\n canAccessTides: boolean,\n): boolean {\n return canAccessTurtletime || canAccessTides;\n}\n\nexport function canDisableAppAccess(\n app: 'turtletime' | 'tides',\n canAccessTurtletime: boolean,\n canAccessTides: boolean,\n): boolean {\n if (app === 'turtletime') return canAccessTides;\n return canAccessTurtletime;\n}\n","/** Per-workspace subscription tier helpers (mirrors core/subscriptions.py). */\n\nexport type SubscriptionTier =\n | 'free'\n | 'plus'\n | 'ai_plus'\n | 'basic'\n | 'pro'\n | 'enterprise';\n\nexport interface WorkspaceSubscription {\n tier: SubscriptionTier;\n tier_label: string;\n limits: {\n seats?: number | null;\n integrated_seats: number | null;\n tides_only_seats: number | null;\n };\n usage: {\n seats?: number;\n integrated_seats: number;\n tides_only_seats: number;\n };\n}\n\nexport type SeatType = 'tides' | 'integrated';\n\nconst TIER_LABELS: Record<SubscriptionTier, string> = {\n free: 'Free',\n plus: 'Plus',\n ai_plus: 'AI Plus',\n basic: 'Basic',\n pro: 'Pro',\n enterprise: 'Enterprise',\n};\n\nexport function tierLabel(tier: SubscriptionTier): string {\n if (tier === 'basic' || tier === 'pro') return 'Plus';\n return TIER_LABELS[tier] ?? 'Free';\n}\n\nexport function resolveSubscriptionTier(\n workspace:\n | {\n subscription?: { tier?: string } | null;\n subscription_tier?: string | null;\n }\n | null\n | undefined,\n): SubscriptionTier {\n const tier = workspace?.subscription?.tier ?? workspace?.subscription_tier;\n if (\n tier === 'plus' ||\n tier === 'ai_plus' ||\n tier === 'basic' ||\n tier === 'pro' ||\n tier === 'enterprise' ||\n tier === 'free'\n ) {\n return tier === 'basic' || tier === 'pro' ? 'plus' : tier;\n }\n return 'free';\n}\n\nexport function tierAllowsTurtletime(tier: SubscriptionTier): boolean {\n return tier !== 'free';\n}\n\nexport function defaultInviteSeatType(tier: SubscriptionTier): SeatType {\n return tierAllowsTurtletime(tier) ? 'integrated' : 'tides';\n}\n\nexport function tierAllowsTidesOnlySeats(): boolean {\n return false;\n}\n\nexport function seatTypeFromFlags(\n canAccessTurtletime: boolean,\n canAccessTides: boolean,\n): SeatType {\n if (canAccessTurtletime && canAccessTides) return 'integrated';\n return 'tides';\n}\n\nexport function flagsFromSeatType(seatType: SeatType): {\n can_access_turtletime: boolean;\n can_access_tides: boolean;\n} {\n if (seatType === 'integrated') {\n return { can_access_turtletime: true, can_access_tides: true };\n }\n return { can_access_turtletime: false, can_access_tides: true };\n}\n\nexport function seatTypeLabel(seatType: SeatType): string {\n return seatType === 'integrated' ? 'Tides + TurtleTime' : 'Tides';\n}\n\nexport function seatTypeDescription(\n tier: SubscriptionTier,\n seatType: SeatType,\n): string {\n if (seatType === 'integrated') {\n return 'Project management and time tracking with TT↔Tides integration.';\n }\n if (!tierAllowsTidesOnlySeats()) {\n return 'Project management included on the Free plan.';\n }\n return 'Project management only — no TurtleTime time tracking.';\n}\n\nexport function planSeatSummary(\n subscription: WorkspaceSubscription | null | undefined,\n): string {\n if (!subscription) return '';\n const { tier, limits, usage } = subscription;\n const seatCap = limits.seats ?? limits.integrated_seats;\n const seatCount = usage.seats ?? usage.integrated_seats;\n if (tier === 'enterprise') {\n return 'Unlimited seats';\n }\n if (seatCap != null) {\n return `${seatCount}/${seatCap} seats`;\n }\n return '';\n}\n","export interface BrandSettingsConfig {\n /** Fallback title when the workspace has not customised one. */\n defaultTitle: string;\n /** Window event dispatched when the brand changes. */\n changedEvent: string;\n}\n\nexport interface WorkspaceBrandPayload {\n brand_logo?: string | null;\n brand_title?: string | null;\n brand_show_title?: boolean;\n}\n\nexport type BrandSettings = ReturnType<typeof createBrandSettings>;\n\nexport function createBrandSettings(config: BrandSettingsConfig) {\n const DEFAULT_BRAND_TITLE = config.defaultTitle;\n const BRAND_SETTINGS_CHANGED = config.changedEvent;\n\n interface WorkspaceBrandState {\n logo: string | null;\n title: string | null;\n showTitle: boolean;\n }\n\n const brandState: WorkspaceBrandState = {\n logo: null,\n title: null,\n showTitle: true,\n };\n\n function notifyBrandChange(): void {\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new Event(BRAND_SETTINGS_CHANGED));\n }\n }\n\n function applyWorkspaceBrand(payload: WorkspaceBrandPayload): void {\n if ('brand_logo' in payload) {\n brandState.logo = payload.brand_logo ?? null;\n }\n if ('brand_title' in payload) {\n const title = payload.brand_title;\n brandState.title = title?.trim() ? title.trim() : null;\n }\n if ('brand_show_title' in payload && payload.brand_show_title !== undefined) {\n brandState.showTitle = payload.brand_show_title;\n }\n notifyBrandChange();\n }\n\n function getBrandLogo(): string | null {\n return brandState.logo;\n }\n\n function getCustomBrandTitle(): string | null {\n return brandState.title;\n }\n\n function getBrandShowTitle(): boolean {\n return brandState.showTitle;\n }\n\n /** Title shown in navigation — null when hidden. */\n function getDisplayBrandTitle(): string | null {\n if (!getBrandShowTitle()) return null;\n const custom = getCustomBrandTitle();\n if (custom === null) return DEFAULT_BRAND_TITLE;\n return custom || DEFAULT_BRAND_TITLE;\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function setBrandLogo(dataUrl: string | null): void {\n brandState.logo = dataUrl;\n notifyBrandChange();\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function setCustomBrandTitle(title: string | null): void {\n brandState.title = title?.trim() ? title.trim() : null;\n notifyBrandChange();\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function setBrandShowTitle(show: boolean): void {\n brandState.showTitle = show;\n notifyBrandChange();\n }\n\n /** @deprecated Brand is persisted via workspace appearance API. */\n function resetBrandTitle(): void {\n brandState.title = null;\n brandState.showTitle = true;\n notifyBrandChange();\n }\n\n return {\n DEFAULT_BRAND_TITLE,\n BRAND_SETTINGS_CHANGED,\n /** @deprecated Use BRAND_SETTINGS_CHANGED */\n BRAND_LOGO_CHANGED: BRAND_SETTINGS_CHANGED,\n applyWorkspaceBrand,\n getBrandLogo,\n getCustomBrandTitle,\n getBrandShowTitle,\n getDisplayBrandTitle,\n setBrandLogo,\n setCustomBrandTitle,\n setBrandShowTitle,\n resetBrandTitle,\n };\n}\n","export interface WorkspaceAppearanceConfig {\n /** Window event dispatched when the active workspace changes. */\n changedEvent: string;\n /** localStorage base key for the theme. */\n themeStorageBase: string;\n /** Reads the active workspace id from the app's API client. */\n getActiveWorkspaceId: () => string | null;\n}\n\nexport type WorkspaceAppearanceHelpers = ReturnType<\n typeof createWorkspaceAppearance\n>;\n\nexport function createWorkspaceAppearance(config: WorkspaceAppearanceConfig) {\n const WORKSPACE_CHANGED = config.changedEvent;\n const THEME_STORAGE_BASE = config.themeStorageBase;\n\n function notifyWorkspaceChanged(): void {\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new Event(WORKSPACE_CHANGED));\n }\n }\n\n function scopedStorageKey(\n base: string,\n workspaceId?: string | null,\n ): string {\n const wsId = workspaceId ?? config.getActiveWorkspaceId();\n return wsId ? `${base}:${wsId}` : base;\n }\n\n function getScopedStorageItem(\n base: string,\n workspaceId?: string | null,\n ): string | null {\n if (typeof window === 'undefined') return null;\n try {\n const scoped = localStorage.getItem(scopedStorageKey(base, workspaceId));\n if (scoped !== null) return scoped;\n const wsId = workspaceId ?? config.getActiveWorkspaceId();\n if (wsId) {\n return localStorage.getItem(base);\n }\n return null;\n } catch {\n return null;\n }\n }\n\n function setScopedStorageItem(\n base: string,\n value: string | null,\n workspaceId?: string | null,\n ): void {\n if (typeof window === 'undefined') return;\n const key = scopedStorageKey(base, workspaceId);\n if (value === null) {\n localStorage.removeItem(key);\n } else {\n localStorage.setItem(key, value);\n }\n }\n\n return {\n WORKSPACE_CHANGED,\n THEME_STORAGE_BASE,\n notifyWorkspaceChanged,\n scopedStorageKey,\n getScopedStorageItem,\n setScopedStorageItem,\n };\n}\n","import type { Api } from '../api/create-api-client';\nimport type { WorkspaceBrandPayload } from './create-brand-settings';\n\nexport interface WorkspaceAppearanceApiConfig {\n api: Api;\n parseApiError: (res: Response) => Promise<string>;\n /** Endpoint base, e.g. `/api/turtletime/workspaces/current/appearance/`. */\n basePath: string;\n applyWorkspaceBrand: (payload: WorkspaceBrandPayload) => void;\n}\n\nexport type WorkspaceTheme = 'light' | 'dark';\n\nexport interface WorkspaceAppearance {\n theme: WorkspaceTheme;\n brand_logo: string | null;\n brand_title: string | null;\n brand_show_title: boolean;\n}\n\nexport type WorkspaceAppearancePatch = Partial<\n Pick<WorkspaceAppearance, 'theme' | 'brand_title' | 'brand_show_title'>\n> & {\n brand_logo_file?: File;\n clear_brand_logo?: boolean;\n};\n\nexport class WorkspaceAppearanceError extends Error {\n status: number;\n\n constructor(message: string, status: number) {\n super(message);\n this.status = status;\n }\n}\n\nfunction normalizeTheme(value: unknown): WorkspaceTheme {\n return value === 'light' ? 'light' : 'dark';\n}\n\nfunction parseAppearancePayload(\n data: Record<string, unknown>,\n): WorkspaceAppearance {\n const raw = (data.appearance ?? data) as Record<string, unknown>;\n return {\n theme: normalizeTheme(raw.theme),\n brand_logo: (raw.brand_logo as string | null) ?? null,\n brand_title: (raw.brand_title as string | null) ?? null,\n brand_show_title: Boolean(raw.brand_show_title),\n };\n}\n\nexport function createWorkspaceAppearanceApi(\n config: WorkspaceAppearanceApiConfig,\n) {\n const { api, parseApiError, basePath, applyWorkspaceBrand } = config;\n\n async function fetchWorkspaceAppearance(): Promise<WorkspaceAppearance> {\n const res = await api.get(basePath);\n if (!res.ok) {\n throw new WorkspaceAppearanceError(await parseApiError(res), res.status);\n }\n const data = await res.json();\n return parseAppearancePayload(data);\n }\n\n async function updateWorkspaceAppearance(\n patch: WorkspaceAppearancePatch,\n ): Promise<WorkspaceAppearance> {\n const hasFileUpload = patch.brand_logo_file instanceof File;\n const shouldClearLogo = patch.clear_brand_logo === true;\n\n if (hasFileUpload || shouldClearLogo) {\n const formData = new FormData();\n if (patch.theme) formData.append('theme', patch.theme);\n if (patch.brand_title !== undefined) {\n formData.append('brand_title', patch.brand_title ?? '');\n }\n if (patch.brand_show_title !== undefined) {\n formData.append(\n 'brand_show_title',\n patch.brand_show_title ? 'true' : 'false',\n );\n }\n if (hasFileUpload && patch.brand_logo_file) {\n formData.append('brand_logo', patch.brand_logo_file);\n }\n if (shouldClearLogo) {\n formData.append('clear_brand_logo', 'true');\n }\n\n const res = await api.patch(basePath, formData);\n if (!res.ok) {\n throw new Error(await parseApiError(res));\n }\n const data = await res.json();\n return parseAppearancePayload(data);\n }\n\n const res = await api.patch(basePath, {\n theme: patch.theme,\n brand_title: patch.brand_title,\n brand_show_title: patch.brand_show_title,\n });\n if (!res.ok) {\n throw new Error(await parseApiError(res));\n }\n const data = await res.json();\n return parseAppearancePayload(data);\n }\n\n function cacheWorkspaceAppearanceLocally(\n appearance: WorkspaceAppearance,\n ): void {\n applyWorkspaceBrand({\n brand_logo: appearance.brand_logo,\n brand_title: appearance.brand_title,\n brand_show_title: appearance.brand_show_title,\n });\n }\n\n return {\n WorkspaceAppearanceError,\n fetchWorkspaceAppearance,\n updateWorkspaceAppearance,\n cacheWorkspaceAppearanceLocally,\n };\n}\n","import type { Api } from '../api/create-api-client';\n\nexport type CrossAppSyncTopic = 'time' | 'integration' | 'tasks';\n\nexport interface CrossAppSyncConfig {\n api: Api;\n /** Checkpoint endpoint, e.g. `/api/turtletime/sync-checkpoint/`. */\n checkpointPath: string;\n /** Poll interval while the tab is visible (ms). */\n pollMs?: number;\n}\n\ntype SyncCheckpoint = {\n tasks_synced_at: number;\n time_synced_at: number;\n integration_synced_at: number;\n};\n\nexport type CrossAppSync = ReturnType<typeof createCrossAppSync>;\n\n/**\n * Poll backend checkpoints so sibling apps refresh when data mutates\n * elsewhere. Same-tab instant updates stay the app's responsibility via its\n * own window events.\n */\nexport function createCrossAppSync(config: CrossAppSyncConfig) {\n const pollMs = config.pollMs ?? 5000;\n\n const lastKnown: Record<CrossAppSyncTopic, number> = {\n time: 0,\n integration: 0,\n tasks: 0,\n };\n\n const subscribers: Array<{\n topic?: CrossAppSyncTopic;\n callback: () => void;\n lastSync: number;\n }> = [];\n\n let pollInterval: number | null = null;\n let pollListeners = 0;\n\n async function fetchCheckpoint(): Promise<SyncCheckpoint | null> {\n try {\n const res = await config.api.get(config.checkpointPath);\n if (!res.ok) return null;\n return (await res.json()) as SyncCheckpoint;\n } catch {\n return null;\n }\n }\n\n function notify(topic: CrossAppSyncTopic): void {\n for (const sub of subscribers) {\n if (!sub.topic || sub.topic === topic) {\n if (lastKnown[topic] > sub.lastSync) {\n sub.lastSync = lastKnown[topic];\n sub.callback();\n }\n }\n }\n }\n\n function checkAndNotify(data: SyncCheckpoint): void {\n const tasks = data.tasks_synced_at || 0;\n const time = data.time_synced_at || 0;\n const integration = data.integration_synced_at || 0;\n\n if (tasks > lastKnown.tasks) {\n lastKnown.tasks = tasks;\n notify('tasks');\n }\n if (time > lastKnown.time) {\n lastKnown.time = time;\n notify('time');\n }\n if (integration > lastKnown.integration) {\n lastKnown.integration = integration;\n notify('integration');\n }\n }\n\n function startPolling(): void {\n if (pollInterval !== null || typeof window === 'undefined') return;\n\n void fetchCheckpoint().then((data) => data && checkAndNotify(data));\n pollInterval = window.setInterval(() => {\n if (document.visibilityState === 'visible') {\n void fetchCheckpoint().then((data) => data && checkAndNotify(data));\n }\n }, pollMs);\n }\n\n function stopPolling(): void {\n if (pollListeners > 0) return;\n if (pollInterval !== null) {\n window.clearInterval(pollInterval);\n pollInterval = null;\n }\n }\n\n function broadcastCrossAppSync(_topic: CrossAppSyncTopic): void {\n // Cross-subdomain sync is handled by backend checkpoints + polling.\n // Same-tab instant updates are dispatched via the app's own events.\n void _topic;\n }\n\n function subscribeCrossAppSync(\n onSync: () => void,\n topic?: CrossAppSyncTopic,\n ): () => void {\n if (typeof window === 'undefined') return () => {};\n\n const sub = {\n topic,\n callback: onSync,\n lastSync: lastKnown[topic || 'tasks'],\n };\n subscribers.push(sub);\n pollListeners += 1;\n startPolling();\n\n return () => {\n const index = subscribers.indexOf(sub);\n if (index >= 0) subscribers.splice(index, 1);\n pollListeners -= 1;\n if (pollListeners <= 0) stopPolling();\n };\n }\n\n function lastCrossAppSyncTime(topic?: CrossAppSyncTopic): number {\n return topic ? lastKnown[topic] : Math.max(...Object.values(lastKnown));\n }\n\n return {\n broadcastCrossAppSync,\n subscribeCrossAppSync,\n lastCrossAppSyncTime,\n };\n}\n","import type {\n CrossAppSync,\n CrossAppSyncTopic,\n} from './create-cross-app-sync';\n\nexport interface LiveRefreshOptions {\n /** Same-tab custom events (e.g. `tt-timer-mutated`). */\n localEvents?: string[];\n /** Gentle poll while the tab is visible (ms). */\n pollMs?: number;\n /** Limit cross-tab refreshes to one mutation topic. */\n crossAppTopic?: CrossAppSyncTopic;\n}\n\nexport type LiveRefresh = ReturnType<typeof createLiveRefresh>;\n\nexport function createLiveRefresh(sync: CrossAppSync) {\n /** True if a cross-tab sync for the given topic happened after `since`. */\n function hasPendingCrossAppSync(\n topic: CrossAppSyncTopic | undefined,\n since: number,\n ): boolean {\n return sync.lastCrossAppSyncTime(topic) > since;\n }\n\n /** Re-run `refresh` when sibling apps mutate data, on same-tab events, or on an interval. */\n function subscribeLiveRefresh(\n refresh: () => void,\n options: LiveRefreshOptions = {},\n ): () => void {\n if (typeof window === 'undefined') return () => {};\n\n const cleanups: Array<() => void> = [];\n let lastRefreshAt = Date.now();\n\n const trigger = () => {\n lastRefreshAt = Date.now();\n refresh();\n };\n\n for (const eventName of options.localEvents ?? []) {\n window.addEventListener(eventName, trigger);\n cleanups.push(() => window.removeEventListener(eventName, trigger));\n }\n\n cleanups.push(sync.subscribeCrossAppSync(trigger, options.crossAppTopic));\n\n const shouldRefreshOnFocus = () =>\n hasPendingCrossAppSync(options.crossAppTopic, lastRefreshAt);\n\n const onVisibility = () => {\n if (document.visibilityState === 'visible' && shouldRefreshOnFocus()) {\n trigger();\n }\n };\n document.addEventListener('visibilitychange', onVisibility);\n cleanups.push(() =>\n document.removeEventListener('visibilitychange', onVisibility),\n );\n\n const onFocus = () => {\n if (shouldRefreshOnFocus()) {\n trigger();\n }\n };\n window.addEventListener('focus', onFocus);\n cleanups.push(() => window.removeEventListener('focus', onFocus));\n\n if (options.pollMs && options.pollMs > 0) {\n const interval = window.setInterval(() => {\n if (document.visibilityState === 'visible') trigger();\n }, options.pollMs);\n cleanups.push(() => window.clearInterval(interval));\n }\n\n return () => {\n for (const cleanup of cleanups) cleanup();\n };\n }\n\n return { subscribeLiveRefresh, hasPendingCrossAppSync };\n}\n","import type { Api } from '../api/create-api-client';\n\n/** Cross-app (suite) notification from `/api/notifications/` — shared across ShellStack apps. */\nexport interface SuiteNotification {\n id: string;\n title: string;\n body: string;\n href: string;\n kind: string;\n source_app: string;\n read_at: string | null;\n created_at: string;\n workspace: string | null;\n organisation: string | null;\n}\n\nexport interface SuiteNotificationsResponse {\n notifications: SuiteNotification[];\n unread_count: number;\n}\n\nexport interface NotificationsClientConfig {\n api: Api;\n parseApiError: (res: Response) => Promise<string>;\n cacheMs?: number;\n listPath?: string;\n}\n\nexport type NotificationsClient = ReturnType<typeof createNotificationsClient>;\n\nexport function createNotificationsClient(config: NotificationsClientConfig) {\n const { api, parseApiError } = config;\n const CACHE_MS = config.cacheMs ?? 15_000;\n const LIST_PATH = config.listPath ?? '/api/notifications/';\n\n let notificationsCache: {\n data: SuiteNotificationsResponse;\n at: number;\n } | null = null;\n let notificationsInflight: Promise<SuiteNotificationsResponse> | null = null;\n\n async function fetchSuiteNotifications(): Promise<SuiteNotificationsResponse> {\n if (notificationsCache && Date.now() - notificationsCache.at < CACHE_MS) {\n return notificationsCache.data;\n }\n if (notificationsInflight) return notificationsInflight;\n notificationsInflight = (async () => {\n try {\n const res = await api.get(LIST_PATH);\n if (!res.ok) throw new Error(await parseApiError(res));\n const data = (await res.json()) as Partial<SuiteNotificationsResponse>;\n const result: SuiteNotificationsResponse = {\n notifications: data.notifications ?? [],\n unread_count: data.unread_count ?? 0,\n };\n notificationsCache = { data: result, at: Date.now() };\n return result;\n } finally {\n notificationsInflight = null;\n }\n })();\n return notificationsInflight;\n }\n\n async function markSuiteNotificationRead(id: string): Promise<void> {\n const res = await api.post(`${LIST_PATH}${id}/read/`);\n if (!res.ok) throw new Error(await parseApiError(res));\n notificationsCache = null;\n }\n\n async function markAllSuiteNotificationsRead(): Promise<void> {\n const res = await api.post(`${LIST_PATH}read-all/`);\n if (!res.ok) throw new Error(await parseApiError(res));\n notificationsCache = null;\n }\n\n return {\n fetchSuiteNotifications,\n markSuiteNotificationRead,\n markAllSuiteNotificationsRead,\n };\n}\n","export type SuiteAiJobEvent = {\n sequence: number;\n event_type:\n | 'queued'\n | 'started'\n | 'progress'\n | 'tool'\n | 'completed'\n | 'failed'\n | 'cancelled';\n summary: string;\n payload: Record<string, unknown>;\n created_at: string;\n};\n\nexport type SuiteAiJob = {\n id: string;\n status: 'pending' | 'running' | 'completed' | 'failed';\n events: SuiteAiJobEvent[];\n};\n\nexport function activeSuiteAiJobs(jobs: SuiteAiJob[]) {\n return jobs.filter(\n (job) => job.status === 'pending' || job.status === 'running',\n );\n}\n\nexport function sortedSuiteAiEvents(events: SuiteAiJobEvent[]) {\n return [...events].sort((a, b) => a.sequence - b.sequence);\n}\n\nexport function suiteAiEventSummary(event?: SuiteAiJobEvent) {\n const summary = event?.summary\n // eslint-disable-next-line no-control-regex\n .replace(/[\\u0000-\\u001f]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n if (summary) return summary;\n if (event?.event_type === 'queued') return 'Shelly request queued';\n if (event?.event_type === 'started')\n return 'Shelly started preparing a reply';\n if (event?.event_type === 'completed') return 'Shelly reply ready';\n if (event?.event_type === 'failed') return 'AI request failed';\n return 'Shelly is working';\n}\n","export const WEBHOOK_ACTIVITY_ACTIONS = [\n { id: 'created', label: 'Task created' },\n { id: 'updated', label: 'Task updated' },\n { id: 'status_changed', label: 'Task status changed' },\n { id: 'assigned', label: 'Task assigned' },\n { id: 'commented', label: 'Comment posted' },\n { id: 'project_renamed', label: 'Project renamed' },\n { id: 'project_status_changed', label: 'Project archived or restored' },\n] as const;\n\nconst ALL_WEBHOOK_ACTION_IDS = WEBHOOK_ACTIVITY_ACTIONS.map(\n (action) => action.id,\n);\n\nexport function webhookActionsToSelected(\n stored: string[] | null | undefined,\n): Set<string> {\n if (!stored || stored.length === 0) {\n return new Set(ALL_WEBHOOK_ACTION_IDS);\n }\n return new Set(stored);\n}\n\nexport function selectedToWebhookActions(selected: Set<string>): string[] {\n if (selected.size >= ALL_WEBHOOK_ACTION_IDS.length) {\n return [];\n }\n return ALL_WEBHOOK_ACTION_IDS.filter((id) => selected.has(id));\n}\n","export interface JsonLdConfig {\n name: string;\n url: string;\n description: string;\n applicationCategory?: string;\n applicationSubCategory?: string;\n operatingSystem?: string;\n softwareVersion?: string;\n price?: string;\n priceCurrency?: string;\n inLanguage?: string;\n publisherName?: string;\n publisherUrl?: string;\n}\n\n/** Build a schema.org SoftwareApplication JSON-LD string for a product page. */\nexport function createJsonLd(config: JsonLdConfig): string {\n const publisherName = config.publisherName ?? 'Xenide';\n const publisherUrl = config.publisherUrl ?? 'https://xenide.io';\n\n return JSON.stringify({\n '@context': 'https://schema.org',\n '@type': 'SoftwareApplication',\n name: config.name,\n url: config.url,\n description: config.description,\n applicationCategory: config.applicationCategory ?? 'BusinessApplication',\n ...(config.applicationSubCategory\n ? { applicationSubCategory: config.applicationSubCategory }\n : {}),\n operatingSystem: config.operatingSystem ?? 'Web',\n browserRequirements: 'Requires JavaScript. Requires HTML5.',\n softwareVersion: config.softwareVersion ?? '1.0',\n offers: {\n '@type': 'Offer',\n price: config.price ?? '0',\n priceCurrency: config.priceCurrency ?? 'AUD',\n },\n publisher: {\n '@type': 'Organization',\n name: publisherName,\n url: publisherUrl,\n },\n author: {\n '@type': 'Organization',\n name: publisherName,\n url: publisherUrl,\n },\n inLanguage: config.inLanguage ?? 'en-AU',\n });\n}\n","'use client';\n\nimport { useEffect, useState } from 'react';\n\nconst NIGHT_START_HOUR = 18;\nconst NIGHT_END_HOUR = 6;\n\nexport function useIsNight(): boolean {\n const [isNight, setIsNight] = useState(false);\n\n useEffect(() => {\n const check = () => {\n const hour = new Date().getHours();\n return hour >= NIGHT_START_HOUR || hour < NIGHT_END_HOUR;\n };\n setIsNight(check());\n\n let timeout: ReturnType<typeof setTimeout>;\n const update = () => {\n const next = check();\n setIsNight((current) => (current !== next ? next : current));\n timeout = setTimeout(update, 60_000);\n };\n timeout = setTimeout(update, 60_000);\n return () => clearTimeout(timeout);\n }, []);\n\n return isNight;\n}\n"],"mappings":"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","names":["res","data"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xenide-io/suite-client",
|
|
3
|
-
"version": "0.2
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Framework-agnostic platform layer shared across Xenide ShellStack apps — API client, auth, workspace, cross-app sync, notifications, errors.",
|
|
5
5
|
"author": "Xenide",
|
|
6
6
|
"license": "MIT",
|