@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 CHANGED
@@ -45,6 +45,9 @@ declare function normalizeThrown(input: unknown, capability?: string): AppError;
45
45
  /** Display string for a failure surface: capability copy plus the code. */
46
46
  declare function formatAppError(error: AppError): string;
47
47
 
48
+ type ApiFetchInit = RequestInit & {
49
+ skipAuth?: boolean;
50
+ };
48
51
  interface ApiClientConfig {
49
52
  /** localStorage key namespace, e.g. `tt` or `tides`. */
50
53
  storagePrefix: string;
@@ -61,10 +64,17 @@ interface ApiClientConfig {
61
64
  workspaceCookieDomain?: string;
62
65
  /** Fired when the active workspace changes in this tab. */
63
66
  onWorkspaceChange?: (id: string | null) => void;
67
+ /** Fired during sign-out before storage is cleared. */
68
+ onSignOut?: () => void;
69
+ /** Clear the workspace key on sign-out (default `true`). */
70
+ clearWorkspaceOnSignOut?: boolean;
71
+ /** Extra localStorage keys removed on sign-out (e.g. portal app tokens). */
72
+ extraStorageKeysOnClear?: string[];
64
73
  /** sessionStorage key prefixes to clear on sign-out. */
65
74
  sessionStorageClearPrefixes?: string[];
75
+ /** Refuse to send the session to an unexpected absolute origin. */
76
+ enforceSameOrigin?: boolean;
66
77
  }
67
- type ApiFetch = (path: string, options?: RequestInit) => Promise<Response>;
68
78
  /**
69
79
  * Build a browser API client scoped to one ShellStack app. Every app shares the
70
80
  * same fetch/refresh/workspace behaviour; only the storage namespace and
@@ -73,9 +83,10 @@ type ApiFetch = (path: string, options?: RequestInit) => Promise<Response>;
73
83
  declare function createApiClient(config: ApiClientConfig): {
74
84
  API_URL: string;
75
85
  onAuthFailure: (handler: () => void) => void;
76
- setTokens: (access: string, refresh: string) => void;
86
+ setTokens: (access: string, refresh?: string | null) => void;
77
87
  getAccessToken: () => string | null;
78
88
  getRefreshToken: () => string | null;
89
+ hasAccessToken: () => boolean;
79
90
  setActiveWorkspaceId: (id: string | null, options?: {
80
91
  share?: boolean;
81
92
  }) => void;
@@ -86,12 +97,14 @@ declare function createApiClient(config: ApiClientConfig): {
86
97
  refreshAccessToken: () => Promise<boolean>;
87
98
  parseApiError: (res: Response) => Promise<string>;
88
99
  api: {
89
- get: (path: string) => Promise<Response>;
90
- post: (path: string, body?: unknown, options?: RequestInit) => Promise<Response>;
91
- put: (path: string, body?: unknown) => Promise<Response>;
92
- patch: (path: string, body?: unknown) => Promise<Response>;
93
- delete: (path: string) => Promise<Response>;
100
+ get: (path: string, init?: ApiFetchInit) => Promise<Response>;
101
+ post: (path: string, body?: unknown, init?: ApiFetchInit) => Promise<Response>;
102
+ put: (path: string, body?: unknown, init?: ApiFetchInit) => Promise<Response>;
103
+ patch: (path: string, body?: unknown, init?: ApiFetchInit) => Promise<Response>;
104
+ delete: (path: string, init?: ApiFetchInit) => Promise<Response>;
94
105
  };
106
+ restoreSession: () => void;
107
+ clearSession: () => void;
95
108
  };
96
109
  type ApiClient = ReturnType<typeof createApiClient>;
97
110
  type Api = ApiClient['api'];
@@ -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 ApiFetch, type AppError, type AuthReturnPath, type AuthReturnPathConfig, type BrandSettings, type BrandSettingsConfig, type CrossAppSync, type CrossAppSyncConfig, type CrossAppSyncTopic, DEFAULT_ERROR_CODE, ERROR_CODES, ERROR_COPY, ERROR_RETRYABLE, type ErrorCode, FREE_PLAN_SEAT_HELP, type JsonLdConfig, type LiveRefresh, type LiveRefreshOptions, type NotificationsClient, type NotificationsClientConfig, ORGANISATION_SEAT_HELP, type PortalHandoffPayload, type PortalHandoffRead, type SeatType, type SubscriptionTier, type SuiteAiJob, type SuiteAiJobEvent, type SuiteNotification, type SuiteNotificationsResponse, TIDES_ACCESS_HELP, TURTLETIME_ACCESS_HELP, WEBHOOK_ACTIVITY_ACTIONS, WORKSPACE_SEAT_HELP, type WorkspaceAppearance, type WorkspaceAppearanceApiConfig, type WorkspaceAppearanceConfig, WorkspaceAppearanceError, type WorkspaceAppearanceHelpers, type WorkspaceAppearancePatch, type WorkspaceBrandPayload, type WorkspaceSubscription, type WorkspaceTheme, activeSuiteAiJobs, appErrorFromResponse, canDisableAppAccess, codeForStatus, copyFor, createApiClient, createAuthReturnPath, createBrandSettings, createCrossAppSync, createJsonLd, createLiveRefresh, createNotificationsClient, createWorkspaceAppearance, createWorkspaceAppearanceApi, defaultInviteSeatType, flagsFromSeatType, formatAppError, hasAppAccess, isErrorCode, normalizeError, normalizeThrown, planSeatSummary, resolveSubscriptionTier, seatTypeDescription, seatTypeFromFlags, seatTypeLabel, selectedToWebhookActions, sortedSuiteAiEvents, suiteAiEventSummary, tierAllowsTidesOnlySeats, tierAllowsTurtletime, tierLabel, useIsNight, webhookActionsToSelected };
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: string) => void;
86
+ setTokens: (access: string, refresh?: string | null) => void;
77
87
  getAccessToken: () => string | null;
78
88
  getRefreshToken: () => string | null;
89
+ hasAccessToken: () => boolean;
79
90
  setActiveWorkspaceId: (id: string | null, options?: {
80
91
  share?: boolean;
81
92
  }) => void;
@@ -86,12 +97,14 @@ declare function createApiClient(config: ApiClientConfig): {
86
97
  refreshAccessToken: () => Promise<boolean>;
87
98
  parseApiError: (res: Response) => Promise<string>;
88
99
  api: {
89
- get: (path: string) => Promise<Response>;
90
- post: (path: string, body?: unknown, options?: RequestInit) => Promise<Response>;
91
- put: (path: string, body?: unknown) => Promise<Response>;
92
- patch: (path: string, body?: unknown) => Promise<Response>;
93
- delete: (path: string) => Promise<Response>;
100
+ get: (path: string, init?: ApiFetchInit) => Promise<Response>;
101
+ post: (path: string, body?: unknown, init?: ApiFetchInit) => Promise<Response>;
102
+ put: (path: string, body?: unknown, init?: ApiFetchInit) => Promise<Response>;
103
+ patch: (path: string, body?: unknown, init?: ApiFetchInit) => Promise<Response>;
104
+ delete: (path: string, init?: ApiFetchInit) => Promise<Response>;
94
105
  };
106
+ restoreSession: () => void;
107
+ clearSession: () => void;
95
108
  };
96
109
  type ApiClient = ReturnType<typeof createApiClient>;
97
110
  type Api = ApiClient['api'];
@@ -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 ApiFetch, type AppError, type AuthReturnPath, type AuthReturnPathConfig, type BrandSettings, type BrandSettingsConfig, type CrossAppSync, type CrossAppSyncConfig, type CrossAppSyncTopic, DEFAULT_ERROR_CODE, ERROR_CODES, ERROR_COPY, ERROR_RETRYABLE, type ErrorCode, FREE_PLAN_SEAT_HELP, type JsonLdConfig, type LiveRefresh, type LiveRefreshOptions, type NotificationsClient, type NotificationsClientConfig, ORGANISATION_SEAT_HELP, type PortalHandoffPayload, type PortalHandoffRead, type SeatType, type SubscriptionTier, type SuiteAiJob, type SuiteAiJobEvent, type SuiteNotification, type SuiteNotificationsResponse, TIDES_ACCESS_HELP, TURTLETIME_ACCESS_HELP, WEBHOOK_ACTIVITY_ACTIONS, WORKSPACE_SEAT_HELP, type WorkspaceAppearance, type WorkspaceAppearanceApiConfig, type WorkspaceAppearanceConfig, WorkspaceAppearanceError, type WorkspaceAppearanceHelpers, type WorkspaceAppearancePatch, type WorkspaceBrandPayload, type WorkspaceSubscription, type WorkspaceTheme, activeSuiteAiJobs, appErrorFromResponse, canDisableAppAccess, codeForStatus, copyFor, createApiClient, createAuthReturnPath, createBrandSettings, createCrossAppSync, createJsonLd, createLiveRefresh, createNotificationsClient, createWorkspaceAppearance, createWorkspaceAppearanceApi, defaultInviteSeatType, flagsFromSeatType, formatAppError, hasAppAccess, isErrorCode, normalizeError, normalizeThrown, planSeatSummary, resolveSubscriptionTier, seatTypeDescription, seatTypeFromFlags, seatTypeLabel, selectedToWebhookActions, sortedSuiteAiEvents, suiteAiEventSummary, tierAllowsTidesOnlySeats, tierAllowsTurtletime, tierLabel, useIsNight, webhookActionsToSelected };
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
- refreshToken = refresh;
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, options = {}) {
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
- ...options.headers
377
+ ...requestInit.headers
367
378
  };
368
- const body = options.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, { ...options, headers });
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
- if (res.status === 401) {
400
- triggerAuthFailure();
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, options = {}) => apiFetch(path, {
412
- ...options,
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
- refreshToken = refresh;
182
+ authFailureFired = false;
183
+ if (refresh !== void 0) refreshToken = refresh;
182
184
  if (typeof window !== "undefined") {
183
185
  window.localStorage.setItem(ACCESS_KEY, access);
184
- window.localStorage.setItem(REFRESH_KEY, refresh);
186
+ if (refresh) window.localStorage.setItem(REFRESH_KEY, refresh);
187
+ else if (refresh !== void 0) window.localStorage.removeItem(REFRESH_KEY);
185
188
  }
186
189
  }
187
190
  function getAccessToken() {
@@ -198,6 +201,9 @@ function createApiClient(config) {
198
201
  }
199
202
  return refreshToken;
200
203
  }
204
+ function hasAccessToken() {
205
+ return Boolean(getAccessToken());
206
+ }
201
207
  function setActiveWorkspaceId(id, options) {
202
208
  const changed = activeWorkspaceId !== id;
203
209
  activeWorkspaceId = id;
@@ -224,13 +230,17 @@ function createApiClient(config) {
224
230
  }
225
231
  }
226
232
  function clearAuth() {
233
+ config.onSignOut?.();
227
234
  accessToken = null;
228
235
  refreshToken = null;
229
236
  activeWorkspaceId = null;
230
237
  if (typeof window !== "undefined") {
231
238
  window.localStorage.removeItem(ACCESS_KEY);
232
239
  window.localStorage.removeItem(REFRESH_KEY);
233
- window.localStorage.removeItem(WORKSPACE_KEY);
240
+ if (CLEAR_WORKSPACE) window.localStorage.removeItem(WORKSPACE_KEY);
241
+ for (const key of config.extraStorageKeysOnClear ?? []) {
242
+ window.localStorage.removeItem(key);
243
+ }
234
244
  const prefixes = config.sessionStorageClearPrefixes ?? [];
235
245
  if (prefixes.length > 0) {
236
246
  try {
@@ -289,36 +299,43 @@ function createApiClient(config) {
289
299
  return false;
290
300
  }
291
301
  }
292
- async function apiFetch(path, options = {}) {
302
+ async function apiFetch(path, init = {}) {
303
+ const { skipAuth, ...requestInit } = init;
293
304
  const token = getAccessToken();
294
305
  const headers = {
295
306
  Accept: "application/json",
296
- ...options.headers
307
+ ...requestInit.headers
297
308
  };
298
- const body = options.body;
309
+ const body = requestInit.body;
299
310
  const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
300
311
  if (!isFormData) {
301
312
  headers["Content-Type"] = "application/json";
302
313
  }
303
- if (token) {
314
+ if (token && !skipAuth) {
304
315
  headers["Authorization"] = `Bearer ${token}`;
305
316
  }
306
317
  const wsId = getActiveWorkspaceId();
307
- if (wsId) {
318
+ if (wsId && !skipAuth) {
308
319
  headers["X-Active-Workspace-Id"] = wsId;
309
320
  }
310
321
  const url = path.startsWith("http") ? path : `${API_URL}${path}`;
322
+ if (config.enforceSameOrigin && !skipAuth && path.startsWith("http") && !url.startsWith(API_URL)) {
323
+ throw new Error("Refusing to send the session to an unexpected origin.");
324
+ }
311
325
  async function doFetch() {
312
326
  try {
313
- return await fetch(url, { ...options, headers });
314
- } catch {
327
+ return await fetch(url, { ...requestInit, headers });
328
+ } catch (error) {
329
+ if (init.signal?.aborted || error instanceof DOMException && error.name === "AbortError") {
330
+ throw error;
331
+ }
315
332
  throw new Error(
316
333
  `Could not reach the API at ${API_URL}. Is the Django backend running?`
317
334
  );
318
335
  }
319
336
  }
320
337
  let res = await doFetch();
321
- if (res.status === 401 && token) {
338
+ if (res.status === 401 && token && !skipAuth) {
322
339
  const refreshed = await refreshAccessToken();
323
340
  if (refreshed) {
324
341
  const newToken = getAccessToken();
@@ -326,9 +343,10 @@ function createApiClient(config) {
326
343
  headers["Authorization"] = `Bearer ${newToken}`;
327
344
  }
328
345
  res = await doFetch();
329
- if (res.status === 401) {
330
- triggerAuthFailure();
331
- }
346
+ }
347
+ if (res.status === 401) {
348
+ clearAuth();
349
+ triggerAuthFailure();
332
350
  }
333
351
  }
334
352
  return res;
@@ -337,21 +355,23 @@ function createApiClient(config) {
337
355
  return formatAppError(await appErrorFromResponse(res));
338
356
  }
339
357
  const api = {
340
- get: (path) => apiFetch(path),
341
- post: (path, body, options = {}) => apiFetch(path, {
342
- ...options,
358
+ get: (path, init) => apiFetch(path, init),
359
+ post: (path, body, init = {}) => apiFetch(path, {
360
+ ...init,
343
361
  method: "POST",
344
362
  body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0
345
363
  }),
346
- put: (path, body) => apiFetch(path, {
364
+ put: (path, body, init = {}) => apiFetch(path, {
365
+ ...init,
347
366
  method: "PUT",
348
367
  body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0
349
368
  }),
350
- patch: (path, body) => apiFetch(path, {
369
+ patch: (path, body, init = {}) => apiFetch(path, {
370
+ ...init,
351
371
  method: "PATCH",
352
372
  body: body instanceof FormData ? body : body ? JSON.stringify(body) : void 0
353
373
  }),
354
- delete: (path) => apiFetch(path, { method: "DELETE" })
374
+ delete: (path, init = {}) => apiFetch(path, { ...init, method: "DELETE" })
355
375
  };
356
376
  return {
357
377
  API_URL,
@@ -359,6 +379,7 @@ function createApiClient(config) {
359
379
  setTokens,
360
380
  getAccessToken,
361
381
  getRefreshToken,
382
+ hasAccessToken,
362
383
  setActiveWorkspaceId,
363
384
  getActiveWorkspaceId,
364
385
  restoreWorkspace,
@@ -366,7 +387,10 @@ function createApiClient(config) {
366
387
  restoreTokens,
367
388
  refreshAccessToken,
368
389
  parseApiError,
369
- api
390
+ api,
391
+ // Aliases used by Kraken/Shelly.
392
+ restoreSession: restoreTokens,
393
+ clearSession: clearAuth
370
394
  };
371
395
  }
372
396
 
@@ -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.0",
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",