@iblai/web-utils 1.11.11 → 1.12.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.js CHANGED
@@ -9200,22 +9200,22 @@ async function syncAuthToCookies(storageService) {
9200
9200
  function hasNonExpiredAuthToken() {
9201
9201
  const token = window.localStorage.getItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
9202
9202
  if (!token) {
9203
- console.log('################### [hasNonExpiredAuthToken] axd token is not defined', token);
9203
+ console.log("################### [hasNonExpiredAuthToken] axd token is not defined", token);
9204
9204
  return false;
9205
9205
  }
9206
9206
  const tokenExpiry = window.localStorage.getItem(LOCAL_STORAGE_KEYS.TOKEN_EXPIRY);
9207
9207
  if (!tokenExpiry) {
9208
- console.log('################### [hasNonExpiredAuthToken] axd token expiry is not defined', tokenExpiry);
9208
+ console.log("################### [hasNonExpiredAuthToken] axd token expiry is not defined", tokenExpiry);
9209
9209
  return true;
9210
9210
  }
9211
9211
  const expiryDate = new Date(tokenExpiry);
9212
9212
  if (isNaN(expiryDate.getTime())) {
9213
- console.log('################### [hasNonExpiredAuthToken] axd token expiry date', expiryDate);
9213
+ console.log("################### [hasNonExpiredAuthToken] axd token expiry date", expiryDate);
9214
9214
  return false;
9215
9215
  }
9216
9216
  const currentDate = new Date();
9217
9217
  if (expiryDate <= currentDate) {
9218
- console.log('################### [hasNonExpiredAuthToken] axd token expiry date is less than current date ', expiryDate, currentDate);
9218
+ console.log("################### [hasNonExpiredAuthToken] axd token expiry date is less than current date ", expiryDate, currentDate);
9219
9219
  return false;
9220
9220
  }
9221
9221
  return true;
@@ -9478,6 +9478,20 @@ function useAuthProvider({ middleware = new Map(), onAuthSuccess, onAuthFailure,
9478
9478
  console.log("[AuthProvider] Redirect already in progress, skipping");
9479
9479
  return;
9480
9480
  }
9481
+ // Defer every auth redirect while a tenant switch is in flight.
9482
+ // handleTenantSwitch() clears the SHARED (cross-tab) localStorage mid-switch,
9483
+ // so every open window transiently sees missing tokens. Without this guard a
9484
+ // bystander window force-logs-out on the absent DM token, which clears
9485
+ // storage + sets a logout-timestamp cookie that wipes the tokens the
9486
+ // switching tab just wrote and cascades the logout across tabs. The TTL lock
9487
+ // (writeTenantSwitchLock) survives the /login/complete -> /sso-login-complete
9488
+ // redirect and is kept fresh across tabs via useTenantSwitchSync, so it
9489
+ // brackets the switch window. The cross-SPA cookie-sync poll keeps running
9490
+ // and recovers once the new tenant's tokens land.
9491
+ if (isTenantSwitchInProgress()) {
9492
+ console.log("[AuthProvider] Tenant switch in progress, deferring auth redirect");
9493
+ return;
9494
+ }
9481
9495
  isRedirectingRef.current = true;
9482
9496
  redirectStartedAtRef.current = Date.now();
9483
9497
  // NOTE: we intentionally do NOT clear the interval here.
@@ -9663,6 +9677,17 @@ function useAuthProvider({ middleware = new Map(), onAuthSuccess, onAuthFailure,
9663
9677
  var _a;
9664
9678
  try {
9665
9679
  setIsAuthenticating(true);
9680
+ // During an in-flight tenant switch the shared, cross-tab localStorage is
9681
+ // transiently cleared (see handleTenantSwitch), so the auth/DM tokens are
9682
+ // legitimately absent. Bail out BEFORE onAuthFailure — some apps hard
9683
+ // navigate to /error/403 from that callback (e.g. skills does
9684
+ // `window.location.href = '/error/403'`), which would fire even though
9685
+ // safeRedirectToAuthSpa itself is guarded. The cookie-sync poll re-checks
9686
+ // once the new tenant's tokens land.
9687
+ if (isTenantSwitchInProgress()) {
9688
+ console.log("[AuthProvider] Tenant switch in progress, skipping auth check");
9689
+ return;
9690
+ }
9666
9691
  const authToken = await (storageService === null || storageService === void 0 ? void 0 : storageService.getItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN));
9667
9692
  if (authToken && !hasNonExpiredAuthToken()) {
9668
9693
  const reason = "Auth token expired";
@@ -11889,46 +11914,117 @@ const selectIsReasoning = (state) => state.chatSliceShared.currentStreamingMessa
11889
11914
  // NEW: Get streaming tool calls
11890
11915
  const selectStreamingToolCalls = (state) => state.chatSliceShared.currentStreamingMessage.toolCalls;
11891
11916
 
11917
+ const DEFAULT_IDLE_TIMEOUT_SECONDS = 120;
11918
+ // Minimum gap between idle-timer resets. Interaction events like mousemove fire
11919
+ // rapidly; there's no need to restart the (minutes-long) idle countdown more
11920
+ // than once per second.
11921
+ const ACTIVITY_THROTTLE_MS = 1000;
11922
+ // User interactions that count as activity and keep the user "present".
11923
+ const ACTIVITY_EVENTS = [
11924
+ "mousemove",
11925
+ "mousedown",
11926
+ "keydown",
11927
+ "wheel",
11928
+ "scroll",
11929
+ "touchstart",
11930
+ "pointerdown",
11931
+ ];
11892
11932
  class TimeTracker {
11893
11933
  constructor(config) {
11934
+ var _a, _b, _c;
11894
11935
  this.state = {
11895
11936
  currentUrl: "",
11896
- startTime: 0,
11897
11937
  intervalId: null,
11898
11938
  routeUnsubscribe: null,
11939
+ accumulatedMs: 0,
11940
+ activeSince: null,
11941
+ isVisible: true,
11942
+ isFocused: true,
11943
+ isIdle: false,
11944
+ idleTimerId: null,
11899
11945
  };
11946
+ // Timestamp of the last activity that reset the idle timer (for throttling).
11947
+ this.lastActivityAt = 0;
11948
+ // Bound listeners kept so they can be removed on destroy.
11949
+ this.handleActivity = () => this.onActivity();
11950
+ this.handleVisibilityChange = () => this.onVisibilityChange();
11951
+ this.handleWindowFocus = () => this.onWindowFocus();
11952
+ this.handleWindowBlur = () => this.onWindowBlur();
11900
11953
  this.config = config;
11954
+ this.idleTimeoutMs =
11955
+ ((_a = config.idleTimeoutSeconds) !== null && _a !== void 0 ? _a : DEFAULT_IDLE_TIMEOUT_SECONDS) * 1000;
11956
+ this.pauseOnHidden = (_b = config.pauseOnHidden) !== null && _b !== void 0 ? _b : true;
11957
+ this.pauseOnBlur = (_c = config.pauseOnBlur) !== null && _c !== void 0 ? _c : true;
11958
+ this.idleDetectionEnabled =
11959
+ this.idleTimeoutMs > 0 &&
11960
+ typeof window !== "undefined" &&
11961
+ typeof document !== "undefined";
11901
11962
  this.initialize();
11902
11963
  }
11903
11964
  initialize() {
11904
11965
  this.state.currentUrl = this.config.getCurrentUrl();
11966
+ // Seed activity signals from the current environment.
11967
+ if (this.pauseOnHidden && typeof document !== "undefined") {
11968
+ this.state.isVisible = !document.hidden;
11969
+ }
11970
+ this.state.isFocused = true;
11971
+ this.state.isIdle = false;
11972
+ this.attachActivityListeners();
11905
11973
  this.startTracking();
11974
+ this.startIdleTimer();
11906
11975
  if (this.config.onRouteChange) {
11907
11976
  this.state.routeUnsubscribe = this.config.onRouteChange(() => {
11908
11977
  this.handleRouteChange();
11909
11978
  });
11910
11979
  }
11911
11980
  }
11981
+ // --- Active time accounting -------------------------------------------------
11982
+ /** Whether time should currently be counted. */
11983
+ isActive() {
11984
+ return this.state.isVisible && this.state.isFocused && !this.state.isIdle;
11985
+ }
11986
+ /**
11987
+ * Reconcile the active window with the current activity signals: open a new
11988
+ * window when the user becomes active, or bank the elapsed time when they
11989
+ * become inactive.
11990
+ */
11991
+ syncActiveWindow() {
11992
+ const active = this.isActive();
11993
+ if (active && this.state.activeSince === null) {
11994
+ this.state.activeSince = Date.now();
11995
+ }
11996
+ else if (!active && this.state.activeSince !== null) {
11997
+ this.state.accumulatedMs += Date.now() - this.state.activeSince;
11998
+ this.state.activeSince = null;
11999
+ }
12000
+ }
12001
+ getTimeSpent() {
12002
+ let total = this.state.accumulatedMs;
12003
+ if (this.state.activeSince !== null) {
12004
+ total += Date.now() - this.state.activeSince;
12005
+ }
12006
+ return total;
12007
+ }
12008
+ resetTimer() {
12009
+ this.state.accumulatedMs = 0;
12010
+ this.state.activeSince = this.isActive() ? Date.now() : null;
12011
+ }
11912
12012
  startTracking() {
11913
- this.state.startTime = Date.now();
11914
- this.clearInterval();
12013
+ this.clearTrackingInterval();
12014
+ this.resetTimer();
12015
+ if (typeof window === "undefined")
12016
+ return;
11915
12017
  this.state.intervalId = window.setInterval(() => {
11916
12018
  this.sendTimeUpdate();
11917
12019
  this.resetTimer();
11918
12020
  }, this.config.intervalSeconds * 1000);
11919
12021
  }
11920
- clearInterval() {
12022
+ clearTrackingInterval() {
11921
12023
  if (this.state.intervalId !== null) {
11922
12024
  clearInterval(this.state.intervalId);
11923
12025
  this.state.intervalId = null;
11924
12026
  }
11925
12027
  }
11926
- resetTimer() {
11927
- this.state.startTime = Date.now();
11928
- }
11929
- getTimeSpent() {
11930
- return Date.now() - this.state.startTime;
11931
- }
11932
12028
  sendTimeUpdate() {
11933
12029
  const timeSpent = this.getTimeSpent();
11934
12030
  if (timeSpent > 0) {
@@ -11943,16 +12039,116 @@ class TimeTracker {
11943
12039
  this.resetTimer();
11944
12040
  }
11945
12041
  }
12042
+ // --- Activity / inactivity detection ---------------------------------------
12043
+ attachActivityListeners() {
12044
+ if (typeof window !== "undefined" && window.addEventListener) {
12045
+ if (this.idleDetectionEnabled) {
12046
+ for (const event of ACTIVITY_EVENTS) {
12047
+ window.addEventListener(event, this.handleActivity, {
12048
+ passive: true,
12049
+ });
12050
+ }
12051
+ }
12052
+ if (this.pauseOnBlur) {
12053
+ window.addEventListener("focus", this.handleWindowFocus);
12054
+ window.addEventListener("blur", this.handleWindowBlur);
12055
+ }
12056
+ }
12057
+ if (this.pauseOnHidden &&
12058
+ typeof document !== "undefined" &&
12059
+ document.addEventListener) {
12060
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
12061
+ }
12062
+ }
12063
+ detachActivityListeners() {
12064
+ if (typeof window !== "undefined" && window.removeEventListener) {
12065
+ for (const event of ACTIVITY_EVENTS) {
12066
+ window.removeEventListener(event, this.handleActivity);
12067
+ }
12068
+ window.removeEventListener("focus", this.handleWindowFocus);
12069
+ window.removeEventListener("blur", this.handleWindowBlur);
12070
+ }
12071
+ if (typeof document !== "undefined" && document.removeEventListener) {
12072
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
12073
+ }
12074
+ }
12075
+ startIdleTimer() {
12076
+ if (!this.idleDetectionEnabled)
12077
+ return;
12078
+ this.clearIdleTimer();
12079
+ this.lastActivityAt = Date.now();
12080
+ this.state.idleTimerId = window.setTimeout(() => {
12081
+ this.markIdle();
12082
+ }, this.idleTimeoutMs);
12083
+ }
12084
+ clearIdleTimer() {
12085
+ if (this.state.idleTimerId !== null) {
12086
+ clearTimeout(this.state.idleTimerId);
12087
+ this.state.idleTimerId = null;
12088
+ }
12089
+ }
12090
+ markIdle() {
12091
+ if (this.state.isIdle)
12092
+ return;
12093
+ this.state.isIdle = true;
12094
+ this.syncActiveWindow();
12095
+ }
12096
+ onActivity() {
12097
+ // Returning from idle must always restart the countdown immediately.
12098
+ if (this.state.isIdle) {
12099
+ this.state.isIdle = false;
12100
+ this.syncActiveWindow();
12101
+ this.startIdleTimer();
12102
+ return;
12103
+ }
12104
+ // Otherwise throttle: no need to reset a minutes-long timer on every
12105
+ // mousemove.
12106
+ if (Date.now() - this.lastActivityAt < ACTIVITY_THROTTLE_MS)
12107
+ return;
12108
+ this.startIdleTimer();
12109
+ }
12110
+ onVisibilityChange() {
12111
+ if (typeof document === "undefined")
12112
+ return;
12113
+ this.state.isVisible = !document.hidden;
12114
+ if (this.state.isVisible) {
12115
+ // Returning to the tab counts as being present again.
12116
+ this.state.isIdle = false;
12117
+ this.startIdleTimer();
12118
+ }
12119
+ this.syncActiveWindow();
12120
+ }
12121
+ onWindowFocus() {
12122
+ this.state.isFocused = true;
12123
+ this.state.isIdle = false;
12124
+ this.startIdleTimer();
12125
+ this.syncActiveWindow();
12126
+ }
12127
+ onWindowBlur() {
12128
+ this.state.isFocused = false;
12129
+ this.syncActiveWindow();
12130
+ }
12131
+ // --- Public API -------------------------------------------------------------
11946
12132
  pause() {
11947
12133
  this.sendTimeUpdate();
11948
- this.clearInterval();
12134
+ this.clearTrackingInterval();
12135
+ this.clearIdleTimer();
12136
+ // Stop accounting until resumed.
12137
+ this.state.accumulatedMs = 0;
12138
+ this.state.activeSince = null;
11949
12139
  }
11950
12140
  resume() {
12141
+ // Treat resume as a fresh, present start so a stale idle flag can't keep
12142
+ // the tracker from counting.
12143
+ this.state.isIdle = false;
11951
12144
  this.startTracking();
12145
+ this.startIdleTimer();
11952
12146
  }
11953
12147
  destroy() {
11954
12148
  this.sendTimeUpdate();
11955
- this.clearInterval();
12149
+ this.clearTrackingInterval();
12150
+ this.clearIdleTimer();
12151
+ this.detachActivityListeners();
11956
12152
  if (this.state.routeUnsubscribe) {
11957
12153
  this.state.routeUnsubscribe();
11958
12154
  this.state.routeUnsubscribe = null;
@@ -11964,6 +12160,10 @@ class TimeTracker {
11964
12160
  getTimeSpentSinceLastReset() {
11965
12161
  return this.getTimeSpent();
11966
12162
  }
12163
+ /** Whether the user is currently considered active (present & interacting). */
12164
+ getIsActive() {
12165
+ return this.isActive();
12166
+ }
11967
12167
  }
11968
12168
 
11969
12169
  function useTimeTracker(config) {