@xenide-io/suite-client 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -9417,8 +9417,12 @@ function createAnalyticsClient(config) {
9417
9417
  defaults,
9418
9418
  person_profiles: "identified_only",
9419
9419
  autocapture: true,
9420
- capture_pageview: true,
9421
- capture_pageleave: true,
9420
+ // Pageviews are captured explicitly via `pageview()` (mount + route
9421
+ // change) so every `$pageview` carries the app/super-properties and the
9422
+ // behaviour is identical across all apps. The SDK's own pageview capture
9423
+ // is disabled to avoid double-counting and `defaults`-dependent behaviour.
9424
+ capture_pageview: false,
9425
+ capture_pageleave: false,
9422
9426
  persistence: "localStorage+cookie",
9423
9427
  cross_subdomain_cookie: true
9424
9428
  });
@@ -9476,6 +9480,7 @@ function createAnalyticsClient(config) {
9476
9480
  if (sessionId) headers["X-POSTHOG-SESSION-ID"] = sessionId;
9477
9481
  return headers;
9478
9482
  }
9483
+ if (isBrowser()) init();
9479
9484
  return {
9480
9485
  init,
9481
9486
  identify,
@@ -9502,6 +9507,7 @@ function createApiClient(config) {
9502
9507
  let accessToken = null;
9503
9508
  let refreshToken = null;
9504
9509
  let activeWorkspaceId = null;
9510
+ const etagBodies = /* @__PURE__ */ new Map();
9505
9511
  let authFailureHandler = null;
9506
9512
  let authFailureFired = false;
9507
9513
  let refreshPromise = null;
@@ -9593,6 +9599,7 @@ function createApiClient(config) {
9593
9599
  window.localStorage.removeItem(WORKSPACE_KEY);
9594
9600
  }
9595
9601
  if (changed) {
9602
+ etagBodies.clear();
9596
9603
  config.onWorkspaceChange?.(id2);
9597
9604
  if (id2 && previousWorkspaceId) {
9598
9605
  track("Workspace Switched", {
@@ -9626,6 +9633,7 @@ function createApiClient(config) {
9626
9633
  accessToken = null;
9627
9634
  refreshToken = null;
9628
9635
  activeWorkspaceId = null;
9636
+ etagBodies.clear();
9629
9637
  if (typeof window !== "undefined") {
9630
9638
  window.localStorage.removeItem(ACCESS_KEY);
9631
9639
  window.localStorage.removeItem(REFRESH_KEY);
@@ -9713,6 +9721,12 @@ function createApiClient(config) {
9713
9721
  headers["X-Active-Workspace-Id"] = wsId;
9714
9722
  }
9715
9723
  applyContextHeaders(headers);
9724
+ const method = (requestInit.method ?? "GET").toUpperCase();
9725
+ const etagKey = `${method}:${wsId ?? ""}:${path}`;
9726
+ if (method === "GET") {
9727
+ const hit = etagBodies.get(etagKey);
9728
+ if (hit) headers["If-None-Match"] = hit.etag;
9729
+ }
9716
9730
  const url = path.startsWith("http") ? path : `${API_URL}${path}`;
9717
9731
  if (config.enforceSameOrigin && !skipAuth && path.startsWith("http") && !url.startsWith(API_URL)) {
9718
9732
  throw new Error("Refusing to send the session to an unexpected origin.");
@@ -9744,6 +9758,26 @@ function createApiClient(config) {
9744
9758
  triggerAuthFailure();
9745
9759
  }
9746
9760
  }
9761
+ if (method === "GET" && res.status === 304) {
9762
+ const hit = etagBodies.get(etagKey);
9763
+ if (hit) {
9764
+ return new Response(hit.body, {
9765
+ status: 200,
9766
+ headers: { "Content-Type": "application/json", ETag: hit.etag }
9767
+ });
9768
+ }
9769
+ }
9770
+ if (method === "GET" && res.ok) {
9771
+ const etag = res.headers.get("ETag");
9772
+ if (etag) {
9773
+ const cachedBody = await res.clone().text();
9774
+ if (etagBodies.size >= 40) {
9775
+ const oldest = etagBodies.keys().next().value;
9776
+ if (oldest) etagBodies.delete(oldest);
9777
+ }
9778
+ etagBodies.set(etagKey, { etag, body: cachedBody });
9779
+ }
9780
+ }
9747
9781
  return res;
9748
9782
  }
9749
9783
  async function parseApiError(res) {
@@ -9838,8 +9872,6 @@ function createAuthReturnPath(config) {
9838
9872
  }
9839
9873
  }
9840
9874
  function readPortalHandoff(fallbackNext = DEFAULT_NEXT) {
9841
- const cached = cachedHandoff(fallbackNext);
9842
- if (cached) return cached;
9843
9875
  const hash = new URLSearchParams(window.location.hash.replace(/^#/, ""));
9844
9876
  const query = new URLSearchParams(window.location.search);
9845
9877
  const nextRaw = hash.get("next") || query.get("next");
@@ -9852,19 +9884,18 @@ function createAuthReturnPath(config) {
9852
9884
  if (result.code || result.token) {
9853
9885
  sessionStorage.setItem(HANDOFF_KEY, JSON.stringify(result));
9854
9886
  }
9855
- return result;
9887
+ return result.code || result.token ? result : cachedHandoff(fallbackNext) ?? result;
9856
9888
  }
9857
9889
  let redeemInflight = null;
9890
+ let redeemKey = "";
9858
9891
  function track(event, properties) {
9859
9892
  try {
9860
9893
  config.onAnalyticsEvent?.(event, properties);
9861
9894
  } catch {
9862
9895
  }
9863
9896
  }
9864
- async function redeemPortalHandoff(apiUrl, fallbackNext = DEFAULT_NEXT) {
9865
- redeemInflight ?? (redeemInflight = (async () => {
9866
- const { code, token, refresh, next } = readPortalHandoff(fallbackNext);
9867
- if (!code) return { code: null, token, refresh, next };
9897
+ async function requestSwitchCode(apiUrl, code) {
9898
+ try {
9868
9899
  const response = await fetch(`${apiUrl.replace(/\/$/, "")}${REDEEM_PATH}`, {
9869
9900
  method: "POST",
9870
9901
  headers: {
@@ -9873,21 +9904,57 @@ function createAuthReturnPath(config) {
9873
9904
  },
9874
9905
  body: JSON.stringify({ code })
9875
9906
  });
9876
- if (!response.ok) {
9907
+ if (!response.ok) return "dead";
9908
+ return await response.json();
9909
+ } catch {
9910
+ return "retry";
9911
+ }
9912
+ }
9913
+ async function redeemPortalHandoff(apiUrl, fallbackNext = DEFAULT_NEXT) {
9914
+ const handoff = readPortalHandoff(fallbackNext);
9915
+ const key = JSON.stringify([apiUrl, handoff]);
9916
+ if (redeemInflight && redeemKey === key) {
9917
+ return redeemInflight.then((result) => {
9918
+ if (result.token) sessionStorage.removeItem(HANDOFF_KEY);
9919
+ return result;
9920
+ });
9921
+ }
9922
+ redeemKey = key;
9923
+ const { code, token, refresh, next } = handoff;
9924
+ redeemInflight = (async () => {
9925
+ if (!code) {
9926
+ sessionStorage.removeItem(HANDOFF_KEY);
9927
+ return { code: null, token, refresh, next };
9928
+ }
9929
+ let outcome = await requestSwitchCode(apiUrl, code);
9930
+ if (outcome === "retry") outcome = await requestSwitchCode(apiUrl, code);
9931
+ if (outcome === "retry") {
9932
+ track("Cross-App Switch", { success: false });
9933
+ return { code: null, token: null, refresh: null, next };
9934
+ }
9935
+ if (outcome === "dead") {
9936
+ sessionStorage.removeItem(HANDOFF_KEY);
9937
+ track("Cross-App Switch", { success: false });
9938
+ return { code: null, token: null, refresh: null, next };
9939
+ }
9940
+ const redeemedToken = outcome.token || outcome.access || null;
9941
+ if (!redeemedToken) {
9942
+ sessionStorage.removeItem(HANDOFF_KEY);
9877
9943
  track("Cross-App Switch", { success: false });
9878
9944
  return { code: null, token: null, refresh: null, next };
9879
9945
  }
9880
- const data = await response.json();
9881
9946
  sessionStorage.removeItem(HANDOFF_KEY);
9882
- const redeemedToken = data.token || data.access || null;
9883
- track("Cross-App Switch", { success: Boolean(redeemedToken) });
9947
+ track("Cross-App Switch", { success: true });
9884
9948
  return {
9885
9949
  code: null,
9886
9950
  token: redeemedToken,
9887
- refresh: data.refresh || null,
9951
+ refresh: outcome.refresh || null,
9888
9952
  next
9889
9953
  };
9890
- })());
9954
+ })().then((result) => {
9955
+ if (!result.token && redeemKey === key) redeemInflight = null;
9956
+ return result;
9957
+ });
9891
9958
  return redeemInflight;
9892
9959
  }
9893
9960
  function readAuthReturnPath() {
@@ -10199,6 +10266,165 @@ function createWorkspaceAppearanceApi(config) {
10199
10266
  };
10200
10267
  }
10201
10268
 
10269
+ // src/cache/persist-query-cache.ts
10270
+ var SUITE_QUERY_CACHE_MAX_AGE_MS = 30 * 60 * 1e3;
10271
+ var MAX_SNAPSHOT_CHARS = 15e5;
10272
+ function suiteCacheBuster(tokenKey, workspaceKey) {
10273
+ if (typeof window === "undefined") return "";
10274
+ let token = null;
10275
+ let workspace = null;
10276
+ try {
10277
+ token = window.localStorage.getItem(tokenKey);
10278
+ workspace = window.localStorage.getItem(workspaceKey) || readCookie("xenide_active_workspace_id");
10279
+ } catch {
10280
+ return "";
10281
+ }
10282
+ const subject = subjectFromAccessToken(token);
10283
+ if (!subject || !workspace) return "";
10284
+ return `${subject}:${workspace}`;
10285
+ }
10286
+ function readJsonSnapshot(key, buster, maxAgeMs) {
10287
+ if (typeof window === "undefined" || !buster) return null;
10288
+ try {
10289
+ const raw = window.localStorage.getItem(key);
10290
+ if (!raw) return null;
10291
+ const parsed = JSON.parse(raw);
10292
+ if (parsed.buster !== buster || typeof parsed.savedAt !== "number") return null;
10293
+ if (Date.now() - parsed.savedAt > maxAgeMs) return null;
10294
+ return parsed.value ?? null;
10295
+ } catch {
10296
+ return null;
10297
+ }
10298
+ }
10299
+ function writeJsonSnapshot(key, buster, value) {
10300
+ if (typeof window === "undefined" || !buster) return;
10301
+ try {
10302
+ const raw = JSON.stringify({ buster, savedAt: Date.now(), value });
10303
+ if (raw.length > MAX_SNAPSHOT_CHARS) return;
10304
+ window.localStorage.setItem(key, raw);
10305
+ } catch {
10306
+ }
10307
+ }
10308
+ function persistSuiteQueries(queryClient, options) {
10309
+ const maxAgeMs = options.maxAgeMs ?? SUITE_QUERY_CACHE_MAX_AGE_MS;
10310
+ restore(queryClient, options, maxAgeMs);
10311
+ let timer = 0;
10312
+ const unsubscribe = queryClient.getQueryCache().subscribe(() => {
10313
+ if (typeof window === "undefined") return;
10314
+ window.clearTimeout(timer);
10315
+ timer = window.setTimeout(() => save(queryClient, options), 400);
10316
+ });
10317
+ return () => {
10318
+ if (typeof window !== "undefined") window.clearTimeout(timer);
10319
+ unsubscribe();
10320
+ };
10321
+ }
10322
+ function restore(queryClient, options, maxAgeMs) {
10323
+ const buster = suiteCacheBuster(options.tokenKey, options.workspaceKey);
10324
+ const stored = readJsonSnapshot(
10325
+ options.storageKey,
10326
+ buster,
10327
+ maxAgeMs
10328
+ );
10329
+ if (!Array.isArray(stored)) return;
10330
+ for (const entry of stored) {
10331
+ if (!entry || !Array.isArray(entry.queryKey) || !allowed(entry.queryKey, options.allow)) {
10332
+ continue;
10333
+ }
10334
+ queryClient.setQueryData(entry.queryKey, entry.data);
10335
+ void queryClient.invalidateQueries({
10336
+ queryKey: entry.queryKey,
10337
+ refetchType: "none"
10338
+ });
10339
+ }
10340
+ }
10341
+ function save(queryClient, options) {
10342
+ const buster = suiteCacheBuster(options.tokenKey, options.workspaceKey);
10343
+ if (!buster) return;
10344
+ const queries = queryClient.getQueryCache().getAll().filter(
10345
+ (query) => query.state.status === "success" && query.state.data !== void 0 && allowed(query.queryKey, options.allow)
10346
+ ).map((query) => ({ queryKey: query.queryKey, data: query.state.data }));
10347
+ writeJsonSnapshot(options.storageKey, buster, queries);
10348
+ }
10349
+ function allowed(queryKey, allow) {
10350
+ return allow.some((rule) => {
10351
+ if (typeof rule === "string") return queryKey[0] === rule;
10352
+ return rule.every((part, index) => queryKey[index] === part);
10353
+ });
10354
+ }
10355
+ function readCookie(name) {
10356
+ if (typeof document === "undefined") return null;
10357
+ const prefix = `${name}=`;
10358
+ const entry = document.cookie.split("; ").find((item) => item.startsWith(prefix));
10359
+ if (!entry) return null;
10360
+ try {
10361
+ return decodeURIComponent(entry.slice(prefix.length)) || null;
10362
+ } catch {
10363
+ return entry.slice(prefix.length) || null;
10364
+ }
10365
+ }
10366
+ function subjectFromAccessToken(token) {
10367
+ if (!token) return "";
10368
+ const segment = token.split(".")[1];
10369
+ if (!segment) return "";
10370
+ try {
10371
+ const base64 = segment.replace(/-/g, "+").replace(/_/g, "/");
10372
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
10373
+ const json = JSON.parse(atob(padded));
10374
+ return typeof json.sub === "string" ? json.sub : "";
10375
+ } catch {
10376
+ return "";
10377
+ }
10378
+ }
10379
+ var SUITE_APP_QUERY_CACHES = {
10380
+ kraken: {
10381
+ storageKey: "kraken-query-cache:v1",
10382
+ tokenKey: "kraken_access_token",
10383
+ workspaceKey: "kraken_active_workspace_id",
10384
+ allow: ["kraken-sidebar"]
10385
+ },
10386
+ shellstack: {
10387
+ storageKey: "shellstack-query-cache:v1",
10388
+ tokenKey: "portal_access_token",
10389
+ workspaceKey: "portal_active_workspace_id",
10390
+ allow: ["calendar-events", "org-workspaces", "organisation", "org-team", "directory"]
10391
+ },
10392
+ tides: {
10393
+ storageKey: "tides-query-cache:v1",
10394
+ tokenKey: "tides_access_token",
10395
+ workspaceKey: "tides_active_workspace_id",
10396
+ allow: [
10397
+ ["tides", "projects"],
10398
+ ["tides", "clients"],
10399
+ ["tides", "workspace-members"],
10400
+ ["tides", "workspace-tags"],
10401
+ ["tides", "project-statuses"]
10402
+ ]
10403
+ },
10404
+ turtletime: {
10405
+ storageKey: "tt-query-cache:v1",
10406
+ tokenKey: "tt_access_token",
10407
+ workspaceKey: "tt_active_workspace_id",
10408
+ allow: [
10409
+ ["tt", "tracker-catalog"],
10410
+ ["tt", "projects"],
10411
+ ["tt", "clients"],
10412
+ ["tt", "categories"]
10413
+ ]
10414
+ },
10415
+ shelly: {
10416
+ storageKey: "shelly-query-cache:v1",
10417
+ tokenKey: "shelly_access_token",
10418
+ workspaceKey: "shelly_active_workspace_id",
10419
+ allow: [
10420
+ ["shelly", "skills"],
10421
+ ["shelly", "crews"],
10422
+ ["shelly", "mcp-servers"],
10423
+ ["shelly", "models"]
10424
+ ]
10425
+ }
10426
+ };
10427
+
10202
10428
  // src/sync/create-cross-app-sync.ts
10203
10429
  function createCrossAppSync(config) {
10204
10430
  const pollMs = config.pollMs ?? 5e3;
@@ -10342,6 +10568,74 @@ function createLiveRefresh(sync) {
10342
10568
  return { subscribeLiveRefresh, hasPendingCrossAppSync };
10343
10569
  }
10344
10570
 
10571
+ // src/sync/use-suite-revisions.ts
10572
+ import { useEffect, useRef } from "react";
10573
+ function nextRevisionTick(seen, ready, data) {
10574
+ if (!data) return { seen, ready, changed: [] };
10575
+ const next = { ...seen };
10576
+ if (!ready) {
10577
+ for (const [topic, value] of Object.entries(data)) {
10578
+ if (typeof value === "number") next[topic] = value;
10579
+ }
10580
+ return { seen: next, ready: true, changed: [] };
10581
+ }
10582
+ const changed = [];
10583
+ for (const [topic, value] of Object.entries(data)) {
10584
+ if (typeof value !== "number" || next[topic] === value) continue;
10585
+ next[topic] = value;
10586
+ changed.push(topic);
10587
+ }
10588
+ return { seen: next, ready: true, changed };
10589
+ }
10590
+ function useSuiteRevisions(options) {
10591
+ const loadRef = useRef(options.load);
10592
+ const onTopicRef = useRef(options.onTopic);
10593
+ loadRef.current = options.load;
10594
+ onTopicRef.current = options.onTopic;
10595
+ const pollMs = options.pollMs ?? 15e3;
10596
+ const scope = options.scope ?? "";
10597
+ const seenScope = useRef(null);
10598
+ useEffect(() => {
10599
+ if (!options.enabled || typeof window === "undefined") return;
10600
+ const switching = seenScope.current !== null && seenScope.current !== scope;
10601
+ seenScope.current = scope;
10602
+ let seen = {};
10603
+ let ready = switching;
10604
+ let timer;
10605
+ const tick = async () => {
10606
+ let data = null;
10607
+ try {
10608
+ data = await loadRef.current();
10609
+ } catch {
10610
+ return;
10611
+ }
10612
+ const step = nextRevisionTick(seen, ready, data);
10613
+ seen = step.seen;
10614
+ ready = step.ready;
10615
+ for (const topic of step.changed) onTopicRef.current(topic);
10616
+ };
10617
+ const stop = () => {
10618
+ if (timer !== void 0) window.clearInterval(timer);
10619
+ timer = void 0;
10620
+ };
10621
+ const start = () => {
10622
+ if (timer !== void 0) return;
10623
+ void tick();
10624
+ timer = window.setInterval(() => void tick(), pollMs);
10625
+ };
10626
+ const onVisibility = () => {
10627
+ if (document.visibilityState === "visible") start();
10628
+ else stop();
10629
+ };
10630
+ if (document.visibilityState === "visible") start();
10631
+ document.addEventListener("visibilitychange", onVisibility);
10632
+ return () => {
10633
+ stop();
10634
+ document.removeEventListener("visibilitychange", onVisibility);
10635
+ };
10636
+ }, [options.enabled, pollMs, scope]);
10637
+ }
10638
+
10345
10639
  // src/notifications/create-notifications-client.ts
10346
10640
  function createNotificationsClient(config) {
10347
10641
  const { api, parseApiError } = config;
@@ -10487,6 +10781,47 @@ function createWebPushClient(config) {
10487
10781
  return { supported, getState, enable, disable };
10488
10782
  }
10489
10783
 
10784
+ // src/notifications/use-email-notification-preference.ts
10785
+ import { useCallback, useEffect as useEffect2, useState } from "react";
10786
+ var EMAIL_PREFERENCE_PATH = "/api/notifications/email/";
10787
+ function useEmailNotificationPreference(api, parseApiError) {
10788
+ const [enabled, setEnabled] = useState(false);
10789
+ const [busy, setBusy] = useState(false);
10790
+ const [error, setError] = useState("");
10791
+ useEffect2(() => {
10792
+ let cancelled = false;
10793
+ api.get(EMAIL_PREFERENCE_PATH).then(async (res) => {
10794
+ if (!res.ok || cancelled) return;
10795
+ const data = await res.json();
10796
+ if (!cancelled) setEnabled(Boolean(data.enabled));
10797
+ }).catch(() => {
10798
+ });
10799
+ return () => {
10800
+ cancelled = true;
10801
+ };
10802
+ }, [api]);
10803
+ const onChange = useCallback(
10804
+ (next) => {
10805
+ setBusy(true);
10806
+ setError("");
10807
+ setEnabled(next);
10808
+ api.patch(EMAIL_PREFERENCE_PATH, { enabled: next }).then(async (res) => {
10809
+ if (!res.ok) {
10810
+ setEnabled(!next);
10811
+ setError(await parseApiError(res));
10812
+ }
10813
+ }).catch((err) => {
10814
+ setEnabled(!next);
10815
+ setError(
10816
+ err instanceof Error ? err.message : "Could not save email notifications."
10817
+ );
10818
+ }).finally(() => setBusy(false));
10819
+ },
10820
+ [api, parseApiError]
10821
+ );
10822
+ return { enabled, busy, error, onChange };
10823
+ }
10824
+
10490
10825
  // src/ai/suite-ai-job.ts
10491
10826
  function activeSuiteAiJobs(jobs) {
10492
10827
  return jobs.filter(
@@ -10568,12 +10903,12 @@ function createJsonLd(config) {
10568
10903
  }
10569
10904
 
10570
10905
  // src/hooks/use-is-night.ts
10571
- import { useEffect, useState } from "react";
10906
+ import { useEffect as useEffect3, useState as useState2 } from "react";
10572
10907
  var NIGHT_START_HOUR = 18;
10573
10908
  var NIGHT_END_HOUR = 6;
10574
10909
  function useIsNight() {
10575
- const [isNight, setIsNight] = useState(false);
10576
- useEffect(() => {
10910
+ const [isNight, setIsNight] = useState2(false);
10911
+ useEffect3(() => {
10577
10912
  const check = () => {
10578
10913
  const hour = (/* @__PURE__ */ new Date()).getHours();
10579
10914
  return hour >= NIGHT_START_HOUR || hour < NIGHT_END_HOUR;
@@ -10597,6 +10932,8 @@ export {
10597
10932
  ERROR_RETRYABLE,
10598
10933
  FREE_PLAN_SEAT_HELP,
10599
10934
  ORGANISATION_SEAT_HELP,
10935
+ SUITE_APP_QUERY_CACHES,
10936
+ SUITE_QUERY_CACHE_MAX_AGE_MS,
10600
10937
  TIDES_ACCESS_HELP,
10601
10938
  TURTLETIME_ACCESS_HELP,
10602
10939
  WEBHOOK_ACTIVITY_ACTIONS,
@@ -10624,9 +10961,12 @@ export {
10624
10961
  formatAppError,
10625
10962
  hasAppAccess,
10626
10963
  isErrorCode,
10964
+ nextRevisionTick,
10627
10965
  normalizeError,
10628
10966
  normalizeThrown,
10967
+ persistSuiteQueries,
10629
10968
  planSeatSummary,
10969
+ readJsonSnapshot,
10630
10970
  resolveSubscriptionTier,
10631
10971
  seatTypeDescription,
10632
10972
  seatTypeFromFlags,
@@ -10634,10 +10974,14 @@ export {
10634
10974
  selectedToWebhookActions,
10635
10975
  sortedSuiteAiEvents,
10636
10976
  suiteAiEventSummary,
10977
+ suiteCacheBuster,
10637
10978
  tierAllowsTidesOnlySeats,
10638
10979
  tierAllowsTurtletime,
10639
10980
  tierLabel,
10981
+ useEmailNotificationPreference,
10640
10982
  useIsNight,
10641
- webhookActionsToSelected
10983
+ useSuiteRevisions,
10984
+ webhookActionsToSelected,
10985
+ writeJsonSnapshot
10642
10986
  };
10643
10987
  //# sourceMappingURL=index.mjs.map