@optifye/dashboard-core 6.11.21 → 6.11.22

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.
Files changed (3) hide show
  1. package/dist/index.js +171 -29
  2. package/dist/index.mjs +171 -29
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2232,6 +2232,61 @@ var clearAuthSnapshot = () => {
2232
2232
  safeStorageRemoveItem(AUTH_SNAPSHOT_STORAGE_KEY);
2233
2233
  };
2234
2234
 
2235
+ // src/lib/auth/authAuditLog.ts
2236
+ var TAB_ID = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 10);
2237
+ var FLUSH_INTERVAL_MS = 3e4;
2238
+ var MAX_QUEUE_SIZE = 50;
2239
+ var queue = [];
2240
+ var flushTimer = null;
2241
+ var apiBaseUrl = "";
2242
+ var getAccessToken = null;
2243
+ var initAuthAuditLog = (baseUrl, tokenGetter) => {
2244
+ apiBaseUrl = baseUrl;
2245
+ getAccessToken = tokenGetter;
2246
+ if (typeof window === "undefined") return;
2247
+ if (!flushTimer) {
2248
+ flushTimer = setInterval(flushAuthAuditLog, FLUSH_INTERVAL_MS);
2249
+ window.addEventListener("visibilitychange", () => {
2250
+ if (document.visibilityState === "hidden") {
2251
+ void flushAuthAuditLog();
2252
+ }
2253
+ });
2254
+ }
2255
+ };
2256
+ var logAuthEvent = (eventType, details = {}) => {
2257
+ queue.push({
2258
+ event_type: eventType,
2259
+ tab_id: TAB_ID,
2260
+ details: {
2261
+ ...details,
2262
+ ts: (/* @__PURE__ */ new Date()).toISOString()
2263
+ }
2264
+ });
2265
+ if (queue.length >= MAX_QUEUE_SIZE) {
2266
+ void flushAuthAuditLog();
2267
+ }
2268
+ };
2269
+ var flushAuthAuditLog = async () => {
2270
+ if (queue.length === 0 || !apiBaseUrl || !getAccessToken) return;
2271
+ const events = queue.splice(0, MAX_QUEUE_SIZE);
2272
+ try {
2273
+ const token = await getAccessToken();
2274
+ if (!token) return;
2275
+ const body = JSON.stringify({ events });
2276
+ const url = `${apiBaseUrl}/api/auth/audit`;
2277
+ await fetch(url, {
2278
+ method: "POST",
2279
+ headers: {
2280
+ "Content-Type": "application/json",
2281
+ Authorization: `Bearer ${token}`
2282
+ },
2283
+ body,
2284
+ keepalive: document.visibilityState === "hidden"
2285
+ });
2286
+ } catch {
2287
+ }
2288
+ };
2289
+
2235
2290
  // src/lib/auth/session.ts
2236
2291
  var DEFAULT_MIN_VALIDITY_MS = 6e4;
2237
2292
  var refreshPromises = /* @__PURE__ */ new WeakMap();
@@ -2241,6 +2296,24 @@ var isInvalidRefreshTokenError = (error) => {
2241
2296
  if (!message.includes("refresh token")) return false;
2242
2297
  return message.includes("invalid") || message.includes("not found") || message.includes("revoked");
2243
2298
  };
2299
+ var getSessionFromStorage = () => {
2300
+ if (typeof window === "undefined") return null;
2301
+ try {
2302
+ for (let i = 0; i < localStorage.length; i++) {
2303
+ const key = localStorage.key(i);
2304
+ if (key?.startsWith("sb-") && key?.endsWith("-auth-token")) {
2305
+ const raw = localStorage.getItem(key);
2306
+ if (!raw) continue;
2307
+ const parsed = JSON.parse(raw);
2308
+ if (parsed?.access_token && parsed?.refresh_token && parsed?.expires_at) {
2309
+ return parsed;
2310
+ }
2311
+ }
2312
+ }
2313
+ } catch {
2314
+ }
2315
+ return null;
2316
+ };
2244
2317
  var isSessionValid = (session, minValidityMs = 0) => {
2245
2318
  if (!session) return false;
2246
2319
  if (!session.expires_at) return true;
@@ -2297,6 +2370,18 @@ var refreshSessionSingleFlight = async (supabase) => {
2297
2370
  }
2298
2371
  const invalidRefreshToken = isInvalidRefreshTokenError(refreshError);
2299
2372
  if (invalidRefreshToken) {
2373
+ const storedSession = getSessionFromStorage();
2374
+ if (storedSession && isSessionValid(storedSession, 0)) {
2375
+ console.log("[Auth] Recovered session from localStorage after invalid refresh token (another tab refreshed)");
2376
+ try {
2377
+ await supabase.auth.setSession({
2378
+ access_token: storedSession.access_token,
2379
+ refresh_token: storedSession.refresh_token
2380
+ });
2381
+ } catch {
2382
+ }
2383
+ return { session: storedSession, error: null, invalidRefreshToken: false };
2384
+ }
2300
2385
  try {
2301
2386
  await supabase.auth.signOut({ scope: "local" });
2302
2387
  } catch (error) {
@@ -10379,6 +10464,15 @@ var AuthProvider = ({ children }) => {
10379
10464
  const [error, setError] = React142.useState(null);
10380
10465
  const [authStatus, setAuthStatus] = React142.useState("loading");
10381
10466
  const [showOnboarding, setShowOnboarding] = React142.useState(false);
10467
+ const auditInitRef = React142.useRef(false);
10468
+ if (!auditInitRef.current && typeof window !== "undefined") {
10469
+ const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000";
10470
+ initAuthAuditLog(backendUrl, async () => {
10471
+ const { data: { session: s } } = await supabase.auth.getSession();
10472
+ return s?.access_token ?? null;
10473
+ });
10474
+ auditInitRef.current = true;
10475
+ }
10382
10476
  const isFetchingRef = React142.useRef(false);
10383
10477
  const lastProcessedSessionRef = React142.useRef(null);
10384
10478
  const hasAuthenticatedRef = React142.useRef(false);
@@ -10542,6 +10636,7 @@ var AuthProvider = ({ children }) => {
10542
10636
  resetRecoveryState();
10543
10637
  if (refreshResult.invalidRefreshToken) {
10544
10638
  console.warn("[AuthContext] Refresh token invalid, redirecting to login");
10639
+ logAuthEvent("forced_logout", { reason: "invalid_refresh_token", trigger: "fetchSession" });
10545
10640
  setAuthStatus("failed");
10546
10641
  clearSessionState();
10547
10642
  await handleAuthRequired(supabase, "session_expired");
@@ -10641,6 +10736,7 @@ var AuthProvider = ({ children }) => {
10641
10736
  const signOut = React142.useCallback(async () => {
10642
10737
  try {
10643
10738
  console.log("[AuthContext] Signing out");
10739
+ logAuthEvent("sign_out_initiated", { trigger: "user_action" });
10644
10740
  setAuthStatus("loading");
10645
10741
  resetRecoveryState();
10646
10742
  clearAuthSnapshot();
@@ -10665,6 +10761,22 @@ var AuthProvider = ({ children }) => {
10665
10761
  return;
10666
10762
  }
10667
10763
  const monitorTokenExpiry = async () => {
10764
+ const storedSession = getSessionFromStorage();
10765
+ if (storedSession && isSessionValid(storedSession, 5 * 60 * 1e3)) {
10766
+ if (storedSession.access_token !== session.access_token) {
10767
+ console.log("[AuthContext] Adopting session refreshed by another tab");
10768
+ logAuthEvent("cross_tab_refresh_adopted", { source: "monitorTokenExpiry" });
10769
+ setTrackedSession(storedSession);
10770
+ try {
10771
+ await supabase.auth.setSession({
10772
+ access_token: storedSession.access_token,
10773
+ refresh_token: storedSession.refresh_token
10774
+ });
10775
+ } catch {
10776
+ }
10777
+ }
10778
+ return;
10779
+ }
10668
10780
  const expiresAt = session.expires_at;
10669
10781
  if (!expiresAt) {
10670
10782
  console.warn("[AuthContext] Session has no expiry time");
@@ -10677,13 +10789,16 @@ var AuthProvider = ({ children }) => {
10677
10789
  console.log(`[AuthContext] Token expires in ${minutesUntilExpiry} minutes`);
10678
10790
  if (minutesUntilExpiry < 5 && timeUntilExpiry > 0) {
10679
10791
  console.warn("[AuthContext] Token expiring soon, attempting refresh...");
10792
+ logAuthEvent("token_refresh_started", { reason: "expiring_soon", minutesUntilExpiry });
10680
10793
  const refreshResult = await refreshSessionSingleFlight(supabase);
10681
10794
  if (isSessionValid(refreshResult.session, 0)) {
10682
10795
  setTrackedSession(refreshResult.session);
10796
+ logAuthEvent("token_refresh_succeeded", { expiresAt: refreshResult.session?.expires_at });
10683
10797
  console.log("[AuthContext] Token refreshed successfully");
10684
10798
  return;
10685
10799
  }
10686
10800
  if (refreshResult.invalidRefreshToken) {
10801
+ logAuthEvent("token_refresh_failed", { reason: "invalid_refresh_token", trigger: "proactive" });
10687
10802
  clearAuthSnapshot();
10688
10803
  resetRecoveryState();
10689
10804
  console.error("[AuthContext] Refresh token invalid during proactive refresh");
@@ -10692,13 +10807,16 @@ var AuthProvider = ({ children }) => {
10692
10807
  }
10693
10808
  if (timeUntilExpiry <= 0) {
10694
10809
  console.warn("[AuthContext] Token has expired, attempting refresh...");
10810
+ logAuthEvent("token_refresh_started", { reason: "expired", minutesUntilExpiry });
10695
10811
  const refreshResult = await refreshSessionSingleFlight(supabase);
10696
10812
  if (isSessionValid(refreshResult.session, 0)) {
10697
10813
  setTrackedSession(refreshResult.session);
10814
+ logAuthEvent("token_refresh_succeeded", { expiresAt: refreshResult.session?.expires_at });
10698
10815
  console.log("[AuthContext] Token refreshed after expiry");
10699
10816
  return;
10700
10817
  }
10701
10818
  if (refreshResult.invalidRefreshToken) {
10819
+ logAuthEvent("token_refresh_failed", { reason: "invalid_refresh_token", trigger: "expired" });
10702
10820
  clearAuthSnapshot();
10703
10821
  resetRecoveryState();
10704
10822
  console.error("[AuthContext] Refresh token invalid after expiry");
@@ -10750,6 +10868,29 @@ var AuthProvider = ({ children }) => {
10750
10868
  window.addEventListener("rbac:refresh-scope", handleScopeRefresh);
10751
10869
  return () => window.removeEventListener("rbac:refresh-scope", handleScopeRefresh);
10752
10870
  }, [fetchSession, session]);
10871
+ React142.useEffect(() => {
10872
+ if (typeof window === "undefined") return;
10873
+ const handleStorageChange = (e) => {
10874
+ if (!e.key?.startsWith("sb-") || !e.key?.endsWith("-auth-token")) return;
10875
+ if (!e.newValue || !session) return;
10876
+ try {
10877
+ const stored = JSON.parse(e.newValue);
10878
+ if (stored?.access_token && stored?.refresh_token && stored?.expires_at && stored.access_token !== session.access_token) {
10879
+ console.log("[AuthContext] Cross-tab token update detected, syncing session");
10880
+ logAuthEvent("cross_tab_refresh_adopted", { source: "storage_event" });
10881
+ setTrackedSession(stored);
10882
+ supabase.auth.setSession({
10883
+ access_token: stored.access_token,
10884
+ refresh_token: stored.refresh_token
10885
+ }).catch(() => {
10886
+ });
10887
+ }
10888
+ } catch {
10889
+ }
10890
+ };
10891
+ window.addEventListener("storage", handleStorageChange);
10892
+ return () => window.removeEventListener("storage", handleStorageChange);
10893
+ }, [session, setTrackedSession, supabase]);
10753
10894
  React142.useEffect(() => {
10754
10895
  if (typeof window === "undefined" || authStatus !== "recovering") {
10755
10896
  return;
@@ -10841,6 +10982,7 @@ var AuthProvider = ({ children }) => {
10841
10982
  }
10842
10983
  if (event === "SIGNED_OUT") {
10843
10984
  console.log("[AuthContext] User signed out");
10985
+ logAuthEvent("signed_out", { trigger: "auth_state_change" });
10844
10986
  resetRecoveryState();
10845
10987
  clearAuthSnapshot();
10846
10988
  clearSessionState();
@@ -11117,7 +11259,7 @@ function useSessionTracking(options = {}) {
11117
11259
  const trackerRef = React142.useRef(null);
11118
11260
  const isTrackingRef = React142.useRef(false);
11119
11261
  const initRef = React142.useRef(false);
11120
- const getAccessToken = React142.useCallback(async () => {
11262
+ const getAccessToken2 = React142.useCallback(async () => {
11121
11263
  return session?.access_token || null;
11122
11264
  }, [session?.access_token]);
11123
11265
  React142.useEffect(() => {
@@ -11125,14 +11267,14 @@ function useSessionTracking(options = {}) {
11125
11267
  return;
11126
11268
  }
11127
11269
  initRef.current = true;
11128
- const apiBaseUrl = config?.apiBaseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "";
11129
- if (!apiBaseUrl) {
11270
+ const apiBaseUrl2 = config?.apiBaseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "";
11271
+ if (!apiBaseUrl2) {
11130
11272
  console.warn("[useSessionTracking] No API base URL configured, session tracking disabled");
11131
11273
  return;
11132
11274
  }
11133
11275
  const tracker = createSessionTracker({
11134
- apiBaseUrl,
11135
- getAccessToken,
11276
+ apiBaseUrl: apiBaseUrl2,
11277
+ getAccessToken: getAccessToken2,
11136
11278
  onSessionStart: (sessionId) => {
11137
11279
  isTrackingRef.current = true;
11138
11280
  onSessionStart?.(sessionId);
@@ -11153,7 +11295,7 @@ function useSessionTracking(options = {}) {
11153
11295
  initRef.current = false;
11154
11296
  isTrackingRef.current = false;
11155
11297
  };
11156
- }, [enabled, isAuthenticated, user?.id, session?.access_token, config?.apiBaseUrl, getAccessToken, onSessionStart, onSessionEnd, user]);
11298
+ }, [enabled, isAuthenticated, user?.id, session?.access_token, config?.apiBaseUrl, getAccessToken2, onSessionStart, onSessionEnd, user]);
11157
11299
  React142.useEffect(() => {
11158
11300
  if (!isAuthenticated && trackerRef.current && isTrackingRef.current) {
11159
11301
  trackerRef.current.endSession("logout");
@@ -19734,9 +19876,9 @@ function useUserUsage(userId, options = {}) {
19734
19876
  const [data, setData] = React142.useState(null);
19735
19877
  const [isLoading, setIsLoading] = React142.useState(true);
19736
19878
  const [error, setError] = React142.useState(null);
19737
- const apiBaseUrl = config?.apiBaseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "";
19879
+ const apiBaseUrl2 = config?.apiBaseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "";
19738
19880
  const fetchData = React142.useCallback(async () => {
19739
- if (!userId || !apiBaseUrl || !session?.access_token) {
19881
+ if (!userId || !apiBaseUrl2 || !session?.access_token) {
19740
19882
  setIsLoading(false);
19741
19883
  return;
19742
19884
  }
@@ -19746,7 +19888,7 @@ function useUserUsage(userId, options = {}) {
19746
19888
  const params = new URLSearchParams();
19747
19889
  if (startDate) params.append("start_date", startDate);
19748
19890
  if (endDate) params.append("end_date", endDate);
19749
- const url = `${apiBaseUrl}/api/usage/user/${userId}${params.toString() ? `?${params}` : ""}`;
19891
+ const url = `${apiBaseUrl2}/api/usage/user/${userId}${params.toString() ? `?${params}` : ""}`;
19750
19892
  const response = await fetch(url, {
19751
19893
  headers: {
19752
19894
  "Authorization": `Bearer ${session.access_token}`
@@ -19764,7 +19906,7 @@ function useUserUsage(userId, options = {}) {
19764
19906
  } finally {
19765
19907
  setIsLoading(false);
19766
19908
  }
19767
- }, [userId, apiBaseUrl, session?.access_token, startDate, endDate]);
19909
+ }, [userId, apiBaseUrl2, session?.access_token, startDate, endDate]);
19768
19910
  React142.useEffect(() => {
19769
19911
  if (enabled) {
19770
19912
  fetchData();
@@ -19786,12 +19928,12 @@ function useCompanyUsersUsage(companyId, options = {}) {
19786
19928
  const [isLoading, setIsLoading] = React142.useState(true);
19787
19929
  const [isTodayLoading, setIsTodayLoading] = React142.useState(true);
19788
19930
  const [error, setError] = React142.useState(null);
19789
- const apiBaseUrl = config?.apiBaseUrl || process.env.NEXT_PUBLIC_BACKEND_URL || "";
19931
+ const apiBaseUrl2 = config?.apiBaseUrl || process.env.NEXT_PUBLIC_BACKEND_URL || "";
19790
19932
  const userRole = user?.role || user?.role_level;
19791
19933
  const canAccess = userRole === "owner" || userRole === "optifye";
19792
- const isEnabled = enabled && canAccess && !!apiBaseUrl;
19934
+ const isEnabled = enabled && canAccess && !!apiBaseUrl2;
19793
19935
  const fetchOwnerReport = React142.useCallback(async () => {
19794
- if (!apiBaseUrl || !session?.access_token || !canAccess || !isEnabled) {
19936
+ if (!apiBaseUrl2 || !session?.access_token || !canAccess || !isEnabled) {
19795
19937
  setIsLoading(false);
19796
19938
  return;
19797
19939
  }
@@ -19806,7 +19948,7 @@ function useCompanyUsersUsage(companyId, options = {}) {
19806
19948
  if (startDate) params.append("start_date", startDate);
19807
19949
  if (endDate) params.append("end_date", endDate);
19808
19950
  if (roleFilter) params.append("role_filter", roleFilter);
19809
- const url = `${apiBaseUrl}/api/usage/owner-report${params.toString() ? `?${params}` : ""}`;
19951
+ const url = `${apiBaseUrl2}/api/usage/owner-report${params.toString() ? `?${params}` : ""}`;
19810
19952
  const response = await fetch(url, {
19811
19953
  headers: {
19812
19954
  "Authorization": `Bearer ${session.access_token}`
@@ -19824,15 +19966,15 @@ function useCompanyUsersUsage(companyId, options = {}) {
19824
19966
  } finally {
19825
19967
  setIsLoading(false);
19826
19968
  }
19827
- }, [apiBaseUrl, session?.access_token, canAccess, isEnabled, startDate, endDate, roleFilter]);
19969
+ }, [apiBaseUrl2, session?.access_token, canAccess, isEnabled, startDate, endDate, roleFilter]);
19828
19970
  const fetchTodayUsage = React142.useCallback(async () => {
19829
- if (!apiBaseUrl || !session?.access_token || !canAccess || !isEnabled) {
19971
+ if (!apiBaseUrl2 || !session?.access_token || !canAccess || !isEnabled) {
19830
19972
  setIsTodayLoading(false);
19831
19973
  return;
19832
19974
  }
19833
19975
  setIsTodayLoading(true);
19834
19976
  try {
19835
- const url = `${apiBaseUrl}/api/usage/today`;
19977
+ const url = `${apiBaseUrl2}/api/usage/today`;
19836
19978
  const response = await fetch(url, {
19837
19979
  headers: {
19838
19980
  "Authorization": `Bearer ${session.access_token}`
@@ -19848,7 +19990,7 @@ function useCompanyUsersUsage(companyId, options = {}) {
19848
19990
  } finally {
19849
19991
  setIsTodayLoading(false);
19850
19992
  }
19851
- }, [apiBaseUrl, session?.access_token, canAccess, isEnabled]);
19993
+ }, [apiBaseUrl2, session?.access_token, canAccess, isEnabled]);
19852
19994
  const fetchAll = React142.useCallback(async () => {
19853
19995
  await Promise.all([
19854
19996
  fetchOwnerReport(),
@@ -19918,9 +20060,9 @@ function useCompanyClipsCost() {
19918
20060
  const hasFetchedOnceRef = React142.useRef(false);
19919
20061
  const canViewClipsCost = user?.role_level === "owner" || user?.role_level === "optifye";
19920
20062
  const companyId = user?.properties?.company_id || user?.company_id || entityConfig.companyId;
19921
- const apiBaseUrl = config?.apiBaseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "";
20063
+ const apiBaseUrl2 = config?.apiBaseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "";
19922
20064
  const fetchData = React142.useCallback(async () => {
19923
- if (!canViewClipsCost || !companyId || !supabase || !apiBaseUrl || !session?.access_token) {
20065
+ if (!canViewClipsCost || !companyId || !supabase || !apiBaseUrl2 || !session?.access_token) {
19924
20066
  setIsLoading(false);
19925
20067
  setData(null);
19926
20068
  hasFetchedOnceRef.current = false;
@@ -19932,7 +20074,7 @@ function useCompanyClipsCost() {
19932
20074
  setError(null);
19933
20075
  try {
19934
20076
  const [statsResponse, linesResult] = await Promise.all([
19935
- fetch(`${apiBaseUrl}/api/classification/company-stats?company_id=${encodeURIComponent(companyId)}`, {
20077
+ fetch(`${apiBaseUrl2}/api/classification/company-stats?company_id=${encodeURIComponent(companyId)}`, {
19936
20078
  headers: {
19937
20079
  "Authorization": `Bearer ${session.access_token}`
19938
20080
  }
@@ -19967,7 +20109,7 @@ function useCompanyClipsCost() {
19967
20109
  hasFetchedOnceRef.current = true;
19968
20110
  setIsLoading(false);
19969
20111
  }
19970
- }, [canViewClipsCost, companyId, supabase, apiBaseUrl, session?.access_token]);
20112
+ }, [canViewClipsCost, companyId, supabase, apiBaseUrl2, session?.access_token]);
19971
20113
  React142.useEffect(() => {
19972
20114
  fetchData();
19973
20115
  }, [fetchData]);
@@ -23503,11 +23645,11 @@ function createRenderStep(runNextFrame) {
23503
23645
  */
23504
23646
  schedule: (callback, keepAlive = false, immediate = false) => {
23505
23647
  const addToCurrentFrame = immediate && isProcessing;
23506
- const queue = addToCurrentFrame ? thisFrame : nextFrame;
23648
+ const queue2 = addToCurrentFrame ? thisFrame : nextFrame;
23507
23649
  if (keepAlive)
23508
23650
  toKeepAlive.add(callback);
23509
- if (!queue.has(callback))
23510
- queue.add(callback);
23651
+ if (!queue2.has(callback))
23652
+ queue2.add(callback);
23511
23653
  return callback;
23512
23654
  },
23513
23655
  /**
@@ -53956,7 +54098,7 @@ var AwardNotificationManager = () => {
53956
54098
  const supabase = useSupabase();
53957
54099
  const { user } = useAuth();
53958
54100
  const router$1 = router.useRouter();
53959
- const [queue, setQueue] = React142.useState([]);
54101
+ const [queue2, setQueue] = React142.useState([]);
53960
54102
  const [activeNotification, setActiveNotification] = React142.useState(null);
53961
54103
  const lastUserIdRef = React142.useRef(null);
53962
54104
  React142.useEffect(() => {
@@ -53977,10 +54119,10 @@ var AwardNotificationManager = () => {
53977
54119
  loadNotifications();
53978
54120
  }, [user, supabase]);
53979
54121
  React142.useEffect(() => {
53980
- if (!activeNotification && queue.length > 0) {
53981
- setActiveNotification(queue[0]);
54122
+ if (!activeNotification && queue2.length > 0) {
54123
+ setActiveNotification(queue2[0]);
53982
54124
  }
53983
- }, [queue, activeNotification]);
54125
+ }, [queue2, activeNotification]);
53984
54126
  const dismissActive = async () => {
53985
54127
  if (!activeNotification) return;
53986
54128
  if (!activeNotification.id.startsWith("mock-")) {
package/dist/index.mjs CHANGED
@@ -2203,6 +2203,61 @@ var clearAuthSnapshot = () => {
2203
2203
  safeStorageRemoveItem(AUTH_SNAPSHOT_STORAGE_KEY);
2204
2204
  };
2205
2205
 
2206
+ // src/lib/auth/authAuditLog.ts
2207
+ var TAB_ID = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 10);
2208
+ var FLUSH_INTERVAL_MS = 3e4;
2209
+ var MAX_QUEUE_SIZE = 50;
2210
+ var queue = [];
2211
+ var flushTimer = null;
2212
+ var apiBaseUrl = "";
2213
+ var getAccessToken = null;
2214
+ var initAuthAuditLog = (baseUrl, tokenGetter) => {
2215
+ apiBaseUrl = baseUrl;
2216
+ getAccessToken = tokenGetter;
2217
+ if (typeof window === "undefined") return;
2218
+ if (!flushTimer) {
2219
+ flushTimer = setInterval(flushAuthAuditLog, FLUSH_INTERVAL_MS);
2220
+ window.addEventListener("visibilitychange", () => {
2221
+ if (document.visibilityState === "hidden") {
2222
+ void flushAuthAuditLog();
2223
+ }
2224
+ });
2225
+ }
2226
+ };
2227
+ var logAuthEvent = (eventType, details = {}) => {
2228
+ queue.push({
2229
+ event_type: eventType,
2230
+ tab_id: TAB_ID,
2231
+ details: {
2232
+ ...details,
2233
+ ts: (/* @__PURE__ */ new Date()).toISOString()
2234
+ }
2235
+ });
2236
+ if (queue.length >= MAX_QUEUE_SIZE) {
2237
+ void flushAuthAuditLog();
2238
+ }
2239
+ };
2240
+ var flushAuthAuditLog = async () => {
2241
+ if (queue.length === 0 || !apiBaseUrl || !getAccessToken) return;
2242
+ const events = queue.splice(0, MAX_QUEUE_SIZE);
2243
+ try {
2244
+ const token = await getAccessToken();
2245
+ if (!token) return;
2246
+ const body = JSON.stringify({ events });
2247
+ const url = `${apiBaseUrl}/api/auth/audit`;
2248
+ await fetch(url, {
2249
+ method: "POST",
2250
+ headers: {
2251
+ "Content-Type": "application/json",
2252
+ Authorization: `Bearer ${token}`
2253
+ },
2254
+ body,
2255
+ keepalive: document.visibilityState === "hidden"
2256
+ });
2257
+ } catch {
2258
+ }
2259
+ };
2260
+
2206
2261
  // src/lib/auth/session.ts
2207
2262
  var DEFAULT_MIN_VALIDITY_MS = 6e4;
2208
2263
  var refreshPromises = /* @__PURE__ */ new WeakMap();
@@ -2212,6 +2267,24 @@ var isInvalidRefreshTokenError = (error) => {
2212
2267
  if (!message.includes("refresh token")) return false;
2213
2268
  return message.includes("invalid") || message.includes("not found") || message.includes("revoked");
2214
2269
  };
2270
+ var getSessionFromStorage = () => {
2271
+ if (typeof window === "undefined") return null;
2272
+ try {
2273
+ for (let i = 0; i < localStorage.length; i++) {
2274
+ const key = localStorage.key(i);
2275
+ if (key?.startsWith("sb-") && key?.endsWith("-auth-token")) {
2276
+ const raw = localStorage.getItem(key);
2277
+ if (!raw) continue;
2278
+ const parsed = JSON.parse(raw);
2279
+ if (parsed?.access_token && parsed?.refresh_token && parsed?.expires_at) {
2280
+ return parsed;
2281
+ }
2282
+ }
2283
+ }
2284
+ } catch {
2285
+ }
2286
+ return null;
2287
+ };
2215
2288
  var isSessionValid = (session, minValidityMs = 0) => {
2216
2289
  if (!session) return false;
2217
2290
  if (!session.expires_at) return true;
@@ -2268,6 +2341,18 @@ var refreshSessionSingleFlight = async (supabase) => {
2268
2341
  }
2269
2342
  const invalidRefreshToken = isInvalidRefreshTokenError(refreshError);
2270
2343
  if (invalidRefreshToken) {
2344
+ const storedSession = getSessionFromStorage();
2345
+ if (storedSession && isSessionValid(storedSession, 0)) {
2346
+ console.log("[Auth] Recovered session from localStorage after invalid refresh token (another tab refreshed)");
2347
+ try {
2348
+ await supabase.auth.setSession({
2349
+ access_token: storedSession.access_token,
2350
+ refresh_token: storedSession.refresh_token
2351
+ });
2352
+ } catch {
2353
+ }
2354
+ return { session: storedSession, error: null, invalidRefreshToken: false };
2355
+ }
2271
2356
  try {
2272
2357
  await supabase.auth.signOut({ scope: "local" });
2273
2358
  } catch (error) {
@@ -10350,6 +10435,15 @@ var AuthProvider = ({ children }) => {
10350
10435
  const [error, setError] = useState(null);
10351
10436
  const [authStatus, setAuthStatus] = useState("loading");
10352
10437
  const [showOnboarding, setShowOnboarding] = useState(false);
10438
+ const auditInitRef = useRef(false);
10439
+ if (!auditInitRef.current && typeof window !== "undefined") {
10440
+ const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000";
10441
+ initAuthAuditLog(backendUrl, async () => {
10442
+ const { data: { session: s } } = await supabase.auth.getSession();
10443
+ return s?.access_token ?? null;
10444
+ });
10445
+ auditInitRef.current = true;
10446
+ }
10353
10447
  const isFetchingRef = useRef(false);
10354
10448
  const lastProcessedSessionRef = useRef(null);
10355
10449
  const hasAuthenticatedRef = useRef(false);
@@ -10513,6 +10607,7 @@ var AuthProvider = ({ children }) => {
10513
10607
  resetRecoveryState();
10514
10608
  if (refreshResult.invalidRefreshToken) {
10515
10609
  console.warn("[AuthContext] Refresh token invalid, redirecting to login");
10610
+ logAuthEvent("forced_logout", { reason: "invalid_refresh_token", trigger: "fetchSession" });
10516
10611
  setAuthStatus("failed");
10517
10612
  clearSessionState();
10518
10613
  await handleAuthRequired(supabase, "session_expired");
@@ -10612,6 +10707,7 @@ var AuthProvider = ({ children }) => {
10612
10707
  const signOut = useCallback(async () => {
10613
10708
  try {
10614
10709
  console.log("[AuthContext] Signing out");
10710
+ logAuthEvent("sign_out_initiated", { trigger: "user_action" });
10615
10711
  setAuthStatus("loading");
10616
10712
  resetRecoveryState();
10617
10713
  clearAuthSnapshot();
@@ -10636,6 +10732,22 @@ var AuthProvider = ({ children }) => {
10636
10732
  return;
10637
10733
  }
10638
10734
  const monitorTokenExpiry = async () => {
10735
+ const storedSession = getSessionFromStorage();
10736
+ if (storedSession && isSessionValid(storedSession, 5 * 60 * 1e3)) {
10737
+ if (storedSession.access_token !== session.access_token) {
10738
+ console.log("[AuthContext] Adopting session refreshed by another tab");
10739
+ logAuthEvent("cross_tab_refresh_adopted", { source: "monitorTokenExpiry" });
10740
+ setTrackedSession(storedSession);
10741
+ try {
10742
+ await supabase.auth.setSession({
10743
+ access_token: storedSession.access_token,
10744
+ refresh_token: storedSession.refresh_token
10745
+ });
10746
+ } catch {
10747
+ }
10748
+ }
10749
+ return;
10750
+ }
10639
10751
  const expiresAt = session.expires_at;
10640
10752
  if (!expiresAt) {
10641
10753
  console.warn("[AuthContext] Session has no expiry time");
@@ -10648,13 +10760,16 @@ var AuthProvider = ({ children }) => {
10648
10760
  console.log(`[AuthContext] Token expires in ${minutesUntilExpiry} minutes`);
10649
10761
  if (minutesUntilExpiry < 5 && timeUntilExpiry > 0) {
10650
10762
  console.warn("[AuthContext] Token expiring soon, attempting refresh...");
10763
+ logAuthEvent("token_refresh_started", { reason: "expiring_soon", minutesUntilExpiry });
10651
10764
  const refreshResult = await refreshSessionSingleFlight(supabase);
10652
10765
  if (isSessionValid(refreshResult.session, 0)) {
10653
10766
  setTrackedSession(refreshResult.session);
10767
+ logAuthEvent("token_refresh_succeeded", { expiresAt: refreshResult.session?.expires_at });
10654
10768
  console.log("[AuthContext] Token refreshed successfully");
10655
10769
  return;
10656
10770
  }
10657
10771
  if (refreshResult.invalidRefreshToken) {
10772
+ logAuthEvent("token_refresh_failed", { reason: "invalid_refresh_token", trigger: "proactive" });
10658
10773
  clearAuthSnapshot();
10659
10774
  resetRecoveryState();
10660
10775
  console.error("[AuthContext] Refresh token invalid during proactive refresh");
@@ -10663,13 +10778,16 @@ var AuthProvider = ({ children }) => {
10663
10778
  }
10664
10779
  if (timeUntilExpiry <= 0) {
10665
10780
  console.warn("[AuthContext] Token has expired, attempting refresh...");
10781
+ logAuthEvent("token_refresh_started", { reason: "expired", minutesUntilExpiry });
10666
10782
  const refreshResult = await refreshSessionSingleFlight(supabase);
10667
10783
  if (isSessionValid(refreshResult.session, 0)) {
10668
10784
  setTrackedSession(refreshResult.session);
10785
+ logAuthEvent("token_refresh_succeeded", { expiresAt: refreshResult.session?.expires_at });
10669
10786
  console.log("[AuthContext] Token refreshed after expiry");
10670
10787
  return;
10671
10788
  }
10672
10789
  if (refreshResult.invalidRefreshToken) {
10790
+ logAuthEvent("token_refresh_failed", { reason: "invalid_refresh_token", trigger: "expired" });
10673
10791
  clearAuthSnapshot();
10674
10792
  resetRecoveryState();
10675
10793
  console.error("[AuthContext] Refresh token invalid after expiry");
@@ -10721,6 +10839,29 @@ var AuthProvider = ({ children }) => {
10721
10839
  window.addEventListener("rbac:refresh-scope", handleScopeRefresh);
10722
10840
  return () => window.removeEventListener("rbac:refresh-scope", handleScopeRefresh);
10723
10841
  }, [fetchSession, session]);
10842
+ useEffect(() => {
10843
+ if (typeof window === "undefined") return;
10844
+ const handleStorageChange = (e) => {
10845
+ if (!e.key?.startsWith("sb-") || !e.key?.endsWith("-auth-token")) return;
10846
+ if (!e.newValue || !session) return;
10847
+ try {
10848
+ const stored = JSON.parse(e.newValue);
10849
+ if (stored?.access_token && stored?.refresh_token && stored?.expires_at && stored.access_token !== session.access_token) {
10850
+ console.log("[AuthContext] Cross-tab token update detected, syncing session");
10851
+ logAuthEvent("cross_tab_refresh_adopted", { source: "storage_event" });
10852
+ setTrackedSession(stored);
10853
+ supabase.auth.setSession({
10854
+ access_token: stored.access_token,
10855
+ refresh_token: stored.refresh_token
10856
+ }).catch(() => {
10857
+ });
10858
+ }
10859
+ } catch {
10860
+ }
10861
+ };
10862
+ window.addEventListener("storage", handleStorageChange);
10863
+ return () => window.removeEventListener("storage", handleStorageChange);
10864
+ }, [session, setTrackedSession, supabase]);
10724
10865
  useEffect(() => {
10725
10866
  if (typeof window === "undefined" || authStatus !== "recovering") {
10726
10867
  return;
@@ -10812,6 +10953,7 @@ var AuthProvider = ({ children }) => {
10812
10953
  }
10813
10954
  if (event === "SIGNED_OUT") {
10814
10955
  console.log("[AuthContext] User signed out");
10956
+ logAuthEvent("signed_out", { trigger: "auth_state_change" });
10815
10957
  resetRecoveryState();
10816
10958
  clearAuthSnapshot();
10817
10959
  clearSessionState();
@@ -11088,7 +11230,7 @@ function useSessionTracking(options = {}) {
11088
11230
  const trackerRef = useRef(null);
11089
11231
  const isTrackingRef = useRef(false);
11090
11232
  const initRef = useRef(false);
11091
- const getAccessToken = useCallback(async () => {
11233
+ const getAccessToken2 = useCallback(async () => {
11092
11234
  return session?.access_token || null;
11093
11235
  }, [session?.access_token]);
11094
11236
  useEffect(() => {
@@ -11096,14 +11238,14 @@ function useSessionTracking(options = {}) {
11096
11238
  return;
11097
11239
  }
11098
11240
  initRef.current = true;
11099
- const apiBaseUrl = config?.apiBaseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "";
11100
- if (!apiBaseUrl) {
11241
+ const apiBaseUrl2 = config?.apiBaseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "";
11242
+ if (!apiBaseUrl2) {
11101
11243
  console.warn("[useSessionTracking] No API base URL configured, session tracking disabled");
11102
11244
  return;
11103
11245
  }
11104
11246
  const tracker = createSessionTracker({
11105
- apiBaseUrl,
11106
- getAccessToken,
11247
+ apiBaseUrl: apiBaseUrl2,
11248
+ getAccessToken: getAccessToken2,
11107
11249
  onSessionStart: (sessionId) => {
11108
11250
  isTrackingRef.current = true;
11109
11251
  onSessionStart?.(sessionId);
@@ -11124,7 +11266,7 @@ function useSessionTracking(options = {}) {
11124
11266
  initRef.current = false;
11125
11267
  isTrackingRef.current = false;
11126
11268
  };
11127
- }, [enabled, isAuthenticated, user?.id, session?.access_token, config?.apiBaseUrl, getAccessToken, onSessionStart, onSessionEnd, user]);
11269
+ }, [enabled, isAuthenticated, user?.id, session?.access_token, config?.apiBaseUrl, getAccessToken2, onSessionStart, onSessionEnd, user]);
11128
11270
  useEffect(() => {
11129
11271
  if (!isAuthenticated && trackerRef.current && isTrackingRef.current) {
11130
11272
  trackerRef.current.endSession("logout");
@@ -19705,9 +19847,9 @@ function useUserUsage(userId, options = {}) {
19705
19847
  const [data, setData] = useState(null);
19706
19848
  const [isLoading, setIsLoading] = useState(true);
19707
19849
  const [error, setError] = useState(null);
19708
- const apiBaseUrl = config?.apiBaseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "";
19850
+ const apiBaseUrl2 = config?.apiBaseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "";
19709
19851
  const fetchData = useCallback(async () => {
19710
- if (!userId || !apiBaseUrl || !session?.access_token) {
19852
+ if (!userId || !apiBaseUrl2 || !session?.access_token) {
19711
19853
  setIsLoading(false);
19712
19854
  return;
19713
19855
  }
@@ -19717,7 +19859,7 @@ function useUserUsage(userId, options = {}) {
19717
19859
  const params = new URLSearchParams();
19718
19860
  if (startDate) params.append("start_date", startDate);
19719
19861
  if (endDate) params.append("end_date", endDate);
19720
- const url = `${apiBaseUrl}/api/usage/user/${userId}${params.toString() ? `?${params}` : ""}`;
19862
+ const url = `${apiBaseUrl2}/api/usage/user/${userId}${params.toString() ? `?${params}` : ""}`;
19721
19863
  const response = await fetch(url, {
19722
19864
  headers: {
19723
19865
  "Authorization": `Bearer ${session.access_token}`
@@ -19735,7 +19877,7 @@ function useUserUsage(userId, options = {}) {
19735
19877
  } finally {
19736
19878
  setIsLoading(false);
19737
19879
  }
19738
- }, [userId, apiBaseUrl, session?.access_token, startDate, endDate]);
19880
+ }, [userId, apiBaseUrl2, session?.access_token, startDate, endDate]);
19739
19881
  useEffect(() => {
19740
19882
  if (enabled) {
19741
19883
  fetchData();
@@ -19757,12 +19899,12 @@ function useCompanyUsersUsage(companyId, options = {}) {
19757
19899
  const [isLoading, setIsLoading] = useState(true);
19758
19900
  const [isTodayLoading, setIsTodayLoading] = useState(true);
19759
19901
  const [error, setError] = useState(null);
19760
- const apiBaseUrl = config?.apiBaseUrl || process.env.NEXT_PUBLIC_BACKEND_URL || "";
19902
+ const apiBaseUrl2 = config?.apiBaseUrl || process.env.NEXT_PUBLIC_BACKEND_URL || "";
19761
19903
  const userRole = user?.role || user?.role_level;
19762
19904
  const canAccess = userRole === "owner" || userRole === "optifye";
19763
- const isEnabled = enabled && canAccess && !!apiBaseUrl;
19905
+ const isEnabled = enabled && canAccess && !!apiBaseUrl2;
19764
19906
  const fetchOwnerReport = useCallback(async () => {
19765
- if (!apiBaseUrl || !session?.access_token || !canAccess || !isEnabled) {
19907
+ if (!apiBaseUrl2 || !session?.access_token || !canAccess || !isEnabled) {
19766
19908
  setIsLoading(false);
19767
19909
  return;
19768
19910
  }
@@ -19777,7 +19919,7 @@ function useCompanyUsersUsage(companyId, options = {}) {
19777
19919
  if (startDate) params.append("start_date", startDate);
19778
19920
  if (endDate) params.append("end_date", endDate);
19779
19921
  if (roleFilter) params.append("role_filter", roleFilter);
19780
- const url = `${apiBaseUrl}/api/usage/owner-report${params.toString() ? `?${params}` : ""}`;
19922
+ const url = `${apiBaseUrl2}/api/usage/owner-report${params.toString() ? `?${params}` : ""}`;
19781
19923
  const response = await fetch(url, {
19782
19924
  headers: {
19783
19925
  "Authorization": `Bearer ${session.access_token}`
@@ -19795,15 +19937,15 @@ function useCompanyUsersUsage(companyId, options = {}) {
19795
19937
  } finally {
19796
19938
  setIsLoading(false);
19797
19939
  }
19798
- }, [apiBaseUrl, session?.access_token, canAccess, isEnabled, startDate, endDate, roleFilter]);
19940
+ }, [apiBaseUrl2, session?.access_token, canAccess, isEnabled, startDate, endDate, roleFilter]);
19799
19941
  const fetchTodayUsage = useCallback(async () => {
19800
- if (!apiBaseUrl || !session?.access_token || !canAccess || !isEnabled) {
19942
+ if (!apiBaseUrl2 || !session?.access_token || !canAccess || !isEnabled) {
19801
19943
  setIsTodayLoading(false);
19802
19944
  return;
19803
19945
  }
19804
19946
  setIsTodayLoading(true);
19805
19947
  try {
19806
- const url = `${apiBaseUrl}/api/usage/today`;
19948
+ const url = `${apiBaseUrl2}/api/usage/today`;
19807
19949
  const response = await fetch(url, {
19808
19950
  headers: {
19809
19951
  "Authorization": `Bearer ${session.access_token}`
@@ -19819,7 +19961,7 @@ function useCompanyUsersUsage(companyId, options = {}) {
19819
19961
  } finally {
19820
19962
  setIsTodayLoading(false);
19821
19963
  }
19822
- }, [apiBaseUrl, session?.access_token, canAccess, isEnabled]);
19964
+ }, [apiBaseUrl2, session?.access_token, canAccess, isEnabled]);
19823
19965
  const fetchAll = useCallback(async () => {
19824
19966
  await Promise.all([
19825
19967
  fetchOwnerReport(),
@@ -19889,9 +20031,9 @@ function useCompanyClipsCost() {
19889
20031
  const hasFetchedOnceRef = useRef(false);
19890
20032
  const canViewClipsCost = user?.role_level === "owner" || user?.role_level === "optifye";
19891
20033
  const companyId = user?.properties?.company_id || user?.company_id || entityConfig.companyId;
19892
- const apiBaseUrl = config?.apiBaseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "";
20034
+ const apiBaseUrl2 = config?.apiBaseUrl || process.env.NEXT_PUBLIC_API_BASE_URL || "";
19893
20035
  const fetchData = useCallback(async () => {
19894
- if (!canViewClipsCost || !companyId || !supabase || !apiBaseUrl || !session?.access_token) {
20036
+ if (!canViewClipsCost || !companyId || !supabase || !apiBaseUrl2 || !session?.access_token) {
19895
20037
  setIsLoading(false);
19896
20038
  setData(null);
19897
20039
  hasFetchedOnceRef.current = false;
@@ -19903,7 +20045,7 @@ function useCompanyClipsCost() {
19903
20045
  setError(null);
19904
20046
  try {
19905
20047
  const [statsResponse, linesResult] = await Promise.all([
19906
- fetch(`${apiBaseUrl}/api/classification/company-stats?company_id=${encodeURIComponent(companyId)}`, {
20048
+ fetch(`${apiBaseUrl2}/api/classification/company-stats?company_id=${encodeURIComponent(companyId)}`, {
19907
20049
  headers: {
19908
20050
  "Authorization": `Bearer ${session.access_token}`
19909
20051
  }
@@ -19938,7 +20080,7 @@ function useCompanyClipsCost() {
19938
20080
  hasFetchedOnceRef.current = true;
19939
20081
  setIsLoading(false);
19940
20082
  }
19941
- }, [canViewClipsCost, companyId, supabase, apiBaseUrl, session?.access_token]);
20083
+ }, [canViewClipsCost, companyId, supabase, apiBaseUrl2, session?.access_token]);
19942
20084
  useEffect(() => {
19943
20085
  fetchData();
19944
20086
  }, [fetchData]);
@@ -23474,11 +23616,11 @@ function createRenderStep(runNextFrame) {
23474
23616
  */
23475
23617
  schedule: (callback, keepAlive = false, immediate = false) => {
23476
23618
  const addToCurrentFrame = immediate && isProcessing;
23477
- const queue = addToCurrentFrame ? thisFrame : nextFrame;
23619
+ const queue2 = addToCurrentFrame ? thisFrame : nextFrame;
23478
23620
  if (keepAlive)
23479
23621
  toKeepAlive.add(callback);
23480
- if (!queue.has(callback))
23481
- queue.add(callback);
23622
+ if (!queue2.has(callback))
23623
+ queue2.add(callback);
23482
23624
  return callback;
23483
23625
  },
23484
23626
  /**
@@ -53927,7 +54069,7 @@ var AwardNotificationManager = () => {
53927
54069
  const supabase = useSupabase();
53928
54070
  const { user } = useAuth();
53929
54071
  const router = useRouter();
53930
- const [queue, setQueue] = useState([]);
54072
+ const [queue2, setQueue] = useState([]);
53931
54073
  const [activeNotification, setActiveNotification] = useState(null);
53932
54074
  const lastUserIdRef = useRef(null);
53933
54075
  useEffect(() => {
@@ -53948,10 +54090,10 @@ var AwardNotificationManager = () => {
53948
54090
  loadNotifications();
53949
54091
  }, [user, supabase]);
53950
54092
  useEffect(() => {
53951
- if (!activeNotification && queue.length > 0) {
53952
- setActiveNotification(queue[0]);
54093
+ if (!activeNotification && queue2.length > 0) {
54094
+ setActiveNotification(queue2[0]);
53953
54095
  }
53954
- }, [queue, activeNotification]);
54096
+ }, [queue2, activeNotification]);
53955
54097
  const dismissActive = async () => {
53956
54098
  if (!activeNotification) return;
53957
54099
  if (!activeNotification.id.startsWith("mock-")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optifye/dashboard-core",
3
- "version": "6.11.21",
3
+ "version": "6.11.22",
4
4
  "description": "Reusable UI & logic for Optifye dashboard",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",