@spidy092/auth-client 3.0.9 โ†’ 3.1.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.
@@ -1,6 +1,6 @@
1
1
  // react/useSessionMonitor.js
2
2
  import { useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { useEffect as useEffect2, useCallback } from "react";
3
+ import { useEffect as useEffect3, useCallback } from "react";
4
4
 
5
5
  // token.js
6
6
  import { jwtDecode } from "jwt-decode";
@@ -232,8 +232,8 @@ function setConfig(customConfig = {}) {
232
232
  // โœ… Auto-detect router mode
233
233
  isRouter: customConfig.isRouter || customConfig.clientKey === "account-ui"
234
234
  };
235
+ enableRefreshTokenPersistence(config.persistRefreshToken);
235
236
  if (config.persistRefreshToken) {
236
- enableRefreshTokenPersistence(true);
237
237
  console.log("\u{1F4E6} Refresh token persistence ENABLED (localStorage on HTTPS)");
238
238
  }
239
239
  console.log(`\u{1F527} Auth Client Mode: ${config.isRouter ? "ROUTER" : "CLIENT"}`, {
@@ -249,6 +249,82 @@ function isRouterMode() {
249
249
  return config.isRouter;
250
250
  }
251
251
 
252
+ // diagnostics.js
253
+ var STORAGE_KEY = "auth_diagnostic_context";
254
+ var LOGIN_LOCK_KEY = "auth_login_lock";
255
+ var randomId = () => {
256
+ var _a;
257
+ if ((_a = globalThis.crypto) == null ? void 0 : _a.randomUUID) return globalThis.crypto.randomUUID();
258
+ return `auth-${Date.now()}-${Math.random().toString(36).slice(2)}`;
259
+ };
260
+ var fingerprint = async (value) => {
261
+ var _a;
262
+ if (!value || !((_a = globalThis.crypto) == null ? void 0 : _a.subtle)) return null;
263
+ const bytes = new TextEncoder().encode(String(value));
264
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
265
+ return [...new Uint8Array(digest)].slice(0, 6).map((byte) => byte.toString(16).padStart(2, "0")).join("");
266
+ };
267
+ function getDiagnosticContext() {
268
+ try {
269
+ const existing = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "null");
270
+ if (existing == null ? void 0 : existing.correlationId) return existing;
271
+ } catch {
272
+ }
273
+ const context = { correlationId: randomId(), startedAt: Date.now() };
274
+ try {
275
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(context));
276
+ } catch {
277
+ }
278
+ return context;
279
+ }
280
+ function resetDiagnosticContext() {
281
+ const context = { correlationId: randomId(), startedAt: Date.now() };
282
+ try {
283
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(context));
284
+ } catch {
285
+ }
286
+ return context;
287
+ }
288
+ function diagnosticHeaders() {
289
+ const { correlationId } = getDiagnosticContext();
290
+ return { "X-Correlation-ID": correlationId, "X-Request-ID": correlationId };
291
+ }
292
+ function acquireLoginLock(clientKey, redirectUri, ttlMs = 5e3) {
293
+ const now = Date.now();
294
+ const signature = `${clientKey}|${redirectUri}`;
295
+ try {
296
+ const current = JSON.parse(sessionStorage.getItem(LOGIN_LOCK_KEY) || "null");
297
+ if ((current == null ? void 0 : current.signature) === signature && now - current.createdAt < ttlMs) return false;
298
+ sessionStorage.setItem(LOGIN_LOCK_KEY, JSON.stringify({ signature, createdAt: now }));
299
+ } catch {
300
+ }
301
+ return true;
302
+ }
303
+ function clearLoginLock() {
304
+ try {
305
+ sessionStorage.removeItem(LOGIN_LOCK_KEY);
306
+ } catch {
307
+ }
308
+ }
309
+ async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
310
+ const context = getDiagnosticContext();
311
+ const safe = {
312
+ eventType: "auth_client_diagnostic",
313
+ event,
314
+ outcome,
315
+ reasonCode,
316
+ correlationId: context.correlationId,
317
+ elapsedMs: Date.now() - context.startedAt,
318
+ clientKey: details.clientKey || null,
319
+ status: details.status || null,
320
+ stateFingerprint: await fingerprint(details.state),
321
+ online: typeof navigator === "undefined" ? null : navigator.onLine
322
+ };
323
+ const method = outcome === "FAILURE" ? "error" : outcome === "WARNING" ? "warn" : "info";
324
+ console[method]("[auth-client]", safe);
325
+ return safe;
326
+ }
327
+
252
328
  // core.js
253
329
  var callbackProcessed = false;
254
330
  function login(clientKeyArg, redirectUriArg) {
@@ -261,14 +337,16 @@ function login(clientKeyArg, redirectUriArg) {
261
337
  } = getConfig();
262
338
  const clientKey = clientKeyArg || defaultClientKey;
263
339
  const redirectUri = redirectUriArg || defaultRedirectUri;
264
- console.log("\u{1F504} Smart Login initiated:", {
265
- mode: isRouterMode() ? "ROUTER" : "CLIENT",
266
- clientKey,
267
- redirectUri
268
- });
269
340
  if (!clientKey || !redirectUri) {
341
+ emitAuthDiagnostic("LOGIN_REJECTED", "FAILURE", "CLIENT_CONFIG_MISSING", { clientKey });
270
342
  throw new Error("Missing clientKey or redirectUri");
271
343
  }
344
+ if (!acquireLoginLock(clientKey, redirectUri)) {
345
+ emitAuthDiagnostic("LOGIN_DUPLICATE_SUPPRESSED", "WARNING", "LOGIN_ALREADY_IN_PROGRESS", { clientKey });
346
+ return false;
347
+ }
348
+ resetDiagnosticContext();
349
+ emitAuthDiagnostic("LOGIN_INITIATED", "PENDING", "NONE", { clientKey });
272
350
  sessionStorage.setItem("originalApp", clientKey);
273
351
  sessionStorage.setItem("returnUrl", redirectUri);
274
352
  if (isRouterMode()) {
@@ -283,13 +361,9 @@ function routerLogin(clientKey, redirectUri) {
283
361
  if (redirectUri) {
284
362
  params.append("redirect_uri", redirectUri);
285
363
  }
364
+ params.append("correlation_id", getDiagnosticContext().correlationId);
286
365
  const query = params.toString();
287
366
  const backendLoginUrl = `${authBaseUrl}/login/${clientKey}${query ? `?${query}` : ""}`;
288
- console.log("\u{1F3ED} Router Login: Direct backend authentication", {
289
- clientKey,
290
- redirectUri,
291
- backendUrl: backendLoginUrl
292
- });
293
367
  window.location.href = backendLoginUrl;
294
368
  }
295
369
  function clientLogin(clientKey, redirectUri) {
@@ -301,11 +375,6 @@ function clientLogin(clientKey, redirectUri) {
301
375
  params.append("redirect_uri", redirectUri);
302
376
  }
303
377
  const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;
304
- console.log("\u{1F504} Client Login: Redirecting to centralized login", {
305
- clientKey,
306
- redirectUri,
307
- centralizedUrl: centralizedLoginUrl
308
- });
309
378
  window.location.href = centralizedLoginUrl;
310
379
  }
311
380
  async function logout(options = {}) {
@@ -328,6 +397,7 @@ async function logout(options = {}) {
328
397
  method: "POST",
329
398
  credentials: "include",
330
399
  headers: {
400
+ ...diagnosticHeaders(),
331
401
  "Authorization": token ? `Bearer ${token}` : "",
332
402
  "Content-Type": "application/json"
333
403
  },
@@ -373,11 +443,19 @@ function handleCallback() {
373
443
  callbackProcessed = false;
374
444
  }
375
445
  callbackProcessed = true;
446
+ clearLoginLock();
376
447
  sessionStorage.removeItem("originalApp");
377
448
  sessionStorage.removeItem("returnUrl");
378
449
  if (error) {
379
450
  const errorDescription = params.get("error_description") || error;
380
- throw new Error(`Authentication failed: ${errorDescription}`);
451
+ const authError = new Error(`Authentication failed: ${errorDescription}`);
452
+ authError.code = error;
453
+ authError.correlationId = getDiagnosticContext().correlationId;
454
+ emitAuthDiagnostic("CALLBACK_REJECTED", "FAILURE", error.toUpperCase(), {
455
+ clientKey: getConfig().clientKey,
456
+ state: params.get("state")
457
+ });
458
+ throw authError;
381
459
  }
382
460
  if (accessToken2) {
383
461
  setToken(accessToken2);
@@ -400,9 +478,19 @@ function handleCallback() {
400
478
  url.searchParams.delete("error_description");
401
479
  window.history.replaceState({}, "", url);
402
480
  console.log("\u2705 Callback processed successfully, token stored");
481
+ emitAuthDiagnostic("CALLBACK_COMPLETED", "SUCCESS", "NONE", {
482
+ clientKey: getConfig().clientKey,
483
+ state: params.get("state")
484
+ });
403
485
  return accessToken2;
404
486
  }
405
- throw new Error("No access token found in callback URL");
487
+ emitAuthDiagnostic("CALLBACK_REJECTED", "FAILURE", "ACCESS_TOKEN_MISSING", {
488
+ clientKey: getConfig().clientKey,
489
+ state: params.get("state")
490
+ });
491
+ const missingTokenError = new Error("No access token found in callback URL");
492
+ missingTokenError.code = "ACCESS_TOKEN_MISSING";
493
+ throw missingTokenError;
406
494
  }
407
495
  function resetCallbackState() {
408
496
  callbackProcessed = false;
@@ -417,7 +505,7 @@ async function refreshToken() {
417
505
  }
418
506
  refreshInProgress = true;
419
507
  refreshPromise = (async () => {
420
- var _a, _b, _c, _d;
508
+ var _a, _b, _c, _d, _e, _f;
421
509
  try {
422
510
  const storedRefreshToken = getRefreshToken();
423
511
  console.log("\u{1F504} Refreshing token:", {
@@ -430,6 +518,7 @@ async function refreshToken() {
430
518
  credentials: "include",
431
519
  // โœ… Include httpOnly cookies (for HTTPS)
432
520
  headers: {
521
+ ...diagnosticHeaders(),
433
522
  "Content-Type": "application/json"
434
523
  }
435
524
  };
@@ -440,8 +529,19 @@ async function refreshToken() {
440
529
  const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
441
530
  if (!response.ok) {
442
531
  const errorText = await response.text();
443
- console.error("\u274C Token refresh failed:", response.status, errorText);
444
- throw new Error(`Refresh failed: ${response.status}`);
532
+ let serverCode = null;
533
+ try {
534
+ serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
535
+ } catch {
536
+ }
537
+ emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
538
+ clientKey,
539
+ status: response.status
540
+ });
541
+ const refreshError = new Error(`Refresh failed: ${response.status}`);
542
+ refreshError.code = serverCode || `HTTP_${response.status}`;
543
+ refreshError.status = response.status;
544
+ throw refreshError;
445
545
  }
446
546
  const data = await response.json();
447
547
  const { access_token, refresh_token: new_refresh_token } = data;
@@ -454,10 +554,11 @@ async function refreshToken() {
454
554
  console.log("\u{1F504} New refresh token stored from rotation");
455
555
  }
456
556
  console.log("\u2705 Token refresh successful, listeners notified");
557
+ emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
457
558
  return access_token;
458
559
  } catch (err) {
459
560
  console.error("\u274C Token refresh error:", err);
460
- const isAuthRejection = ((_a = err.message) == null ? void 0 : _a.includes("401")) || ((_b = err.message) == null ? void 0 : _b.includes("403")) || ((_c = err.message) == null ? void 0 : _c.includes("invalid_grant")) || ((_d = err.message) == null ? void 0 : _d.includes("Refresh failed: 4"));
561
+ const isAuthRejection = ((_c = err.message) == null ? void 0 : _c.includes("401")) || ((_d = err.message) == null ? void 0 : _d.includes("403")) || ((_e = err.message) == null ? void 0 : _e.includes("invalid_grant")) || ((_f = err.message) == null ? void 0 : _f.includes("Refresh failed: 4"));
461
562
  if (isAuthRejection) {
462
563
  clearToken();
463
564
  clearRefreshToken();
@@ -480,6 +581,7 @@ async function validateCurrentSession() {
480
581
  const response = await fetch(`${authBaseUrl}/account/validate-session`, {
481
582
  method: "GET",
482
583
  headers: {
584
+ ...diagnosticHeaders(),
483
585
  "Authorization": `Bearer ${token}`,
484
586
  "Content-Type": "application/json"
485
587
  },
@@ -732,6 +834,7 @@ api.interceptors.request.use((config2) => {
732
834
  if ((runtimeConfig == null ? void 0 : runtimeConfig.clientKey) && !config2.headers["X-Client-Key"]) {
733
835
  config2.headers["X-Client-Key"] = runtimeConfig.clientKey;
734
836
  }
837
+ Object.assign(config2.headers, diagnosticHeaders());
735
838
  const token = getToken();
736
839
  if (token) {
737
840
  config2.headers.Authorization = `Bearer ${token}`;
@@ -750,6 +853,10 @@ api.interceptors.response.use(
750
853
  return Promise.reject(error);
751
854
  }
752
855
  config2._retry = true;
856
+ emitAuthDiagnostic("API_401_REFRESH_STARTED", "PENDING", "HTTP_401", {
857
+ clientKey: getConfig().clientKey,
858
+ status: 401
859
+ });
753
860
  if (!refreshPromise2) {
754
861
  refreshPromise2 = refreshToken().then((newToken) => {
755
862
  refreshPromise2 = null;
@@ -809,6 +916,208 @@ function isAuthenticated() {
809
916
  return !!token && !isTokenExpired(token, 10);
810
917
  }
811
918
 
919
+ // preferences.js
920
+ var PREFERENCE_DEFAULTS = Object.freeze({
921
+ theme: "system",
922
+ colorScheme: "blue",
923
+ density: "comfortable",
924
+ reducedMotion: false,
925
+ language: "en",
926
+ timezone: "Etc/UTC",
927
+ dateFormat: "MM/DD/YYYY",
928
+ timeFormat: "12h",
929
+ highContrast: false,
930
+ largeText: false
931
+ });
932
+ var ALLOWED = {
933
+ theme: /* @__PURE__ */ new Set(["light", "dark", "system"]),
934
+ density: /* @__PURE__ */ new Set(["compact", "comfortable", "spacious"]),
935
+ timeFormat: /* @__PURE__ */ new Set(["12h", "24h"])
936
+ };
937
+ var listeners2 = /* @__PURE__ */ new Set();
938
+ var cachedPreferences = null;
939
+ var pendingRequest = null;
940
+ var syncCleanup = null;
941
+ function extractResponseData(response) {
942
+ var _a;
943
+ return ((_a = response == null ? void 0 : response.data) == null ? void 0 : _a.data) ?? (response == null ? void 0 : response.data) ?? response ?? {};
944
+ }
945
+ function normalizePreferences(value = {}) {
946
+ const next = { ...PREFERENCE_DEFAULTS };
947
+ for (const key of Object.keys(PREFERENCE_DEFAULTS)) {
948
+ if (!Object.hasOwn(value, key)) continue;
949
+ const candidate = value[key];
950
+ if (ALLOWED[key] && !ALLOWED[key].has(candidate)) continue;
951
+ if (typeof PREFERENCE_DEFAULTS[key] !== typeof candidate) continue;
952
+ next[key] = candidate;
953
+ }
954
+ return next;
955
+ }
956
+ function resolveTheme(theme, matchMedia2 = globalThis.matchMedia) {
957
+ if (theme === "dark" || theme === "light") return theme;
958
+ return typeof matchMedia2 === "function" && matchMedia2("(prefers-color-scheme: dark)").matches ? "dark" : "light";
959
+ }
960
+ function announce(preferences2) {
961
+ cachedPreferences = normalizePreferences(preferences2);
962
+ listeners2.forEach((listener) => listener(cachedPreferences));
963
+ return cachedPreferences;
964
+ }
965
+ async function getPreferences({ force = false } = {}) {
966
+ if (cachedPreferences && !force) return cachedPreferences;
967
+ if (pendingRequest) return pendingRequest;
968
+ pendingRequest = api_default.get("/account/preferences").then((response) => announce(extractResponseData(response))).finally(() => {
969
+ pendingRequest = null;
970
+ });
971
+ return pendingRequest;
972
+ }
973
+ async function updatePreferences(patch) {
974
+ const allowedPatch = {};
975
+ for (const key of Object.keys(PREFERENCE_DEFAULTS)) {
976
+ if (Object.hasOwn(patch, key)) allowedPatch[key] = patch[key];
977
+ }
978
+ const candidate = normalizePreferences({ ...cachedPreferences || PREFERENCE_DEFAULTS, ...allowedPatch });
979
+ const response = await api_default.put("/account/preferences", allowedPatch);
980
+ const saved = extractResponseData(response);
981
+ return announce(Object.keys(saved).length ? saved : candidate);
982
+ }
983
+ function subscribePreferences(listener) {
984
+ listeners2.add(listener);
985
+ if (cachedPreferences) listener(cachedPreferences);
986
+ return () => listeners2.delete(listener);
987
+ }
988
+ function clearPreferenceCache() {
989
+ cachedPreferences = null;
990
+ pendingRequest = null;
991
+ }
992
+ function applyPreferences(preferences2, documentRef = globalThis.document) {
993
+ var _a, _b, _c;
994
+ if (!(documentRef == null ? void 0 : documentRef.documentElement)) return;
995
+ const value = normalizePreferences(preferences2);
996
+ const root = documentRef.documentElement;
997
+ const resolvedTheme = resolveTheme(value.theme, (_b = (_a = documentRef.defaultView) == null ? void 0 : _a.matchMedia) == null ? void 0 : _b.bind(documentRef.defaultView));
998
+ root.dataset.theme = resolvedTheme;
999
+ root.dataset.themePreference = value.theme;
1000
+ root.dataset.colorScheme = value.colorScheme;
1001
+ root.dataset.density = value.density;
1002
+ root.lang = value.language;
1003
+ root.style.colorScheme = resolvedTheme;
1004
+ root.style.setProperty("--user-accent-color", {
1005
+ blue: "#1976d2",
1006
+ green: "#2e7d32",
1007
+ purple: "#7b1fa2",
1008
+ orange: "#c65d00",
1009
+ red: "#c62828"
1010
+ }[value.colorScheme] || "#1976d2");
1011
+ root.classList.toggle("user-reduced-motion", value.reducedMotion);
1012
+ root.classList.toggle("user-high-contrast", value.highContrast);
1013
+ root.classList.toggle("user-large-text", value.largeText);
1014
+ const styleId = "sso-user-preference-styles";
1015
+ if (!documentRef.getElementById(styleId)) {
1016
+ const style = documentRef.createElement("style");
1017
+ style.id = styleId;
1018
+ style.textContent = `
1019
+ html.user-large-text { font-size: 112.5%; }
1020
+ html.user-high-contrast { filter: contrast(1.15); }
1021
+ html[data-density="compact"] { --user-density-padding: 6px; }
1022
+ html[data-density="comfortable"] { --user-density-padding: 10px; }
1023
+ html[data-density="spacious"] { --user-density-padding: 14px; }
1024
+ html[data-density] .MuiListItem-root { padding-top: var(--user-density-padding); padding-bottom: var(--user-density-padding); }
1025
+ html.user-reduced-motion *, html.user-reduced-motion *::before, html.user-reduced-motion *::after {
1026
+ animation-duration: 0.01ms !important;
1027
+ animation-iteration-count: 1 !important;
1028
+ scroll-behavior: auto !important;
1029
+ transition-duration: 0.01ms !important;
1030
+ }
1031
+ `;
1032
+ (_c = documentRef.head) == null ? void 0 : _c.appendChild(style);
1033
+ }
1034
+ }
1035
+ function startPreferenceSync({ refreshOnFocus = true } = {}) {
1036
+ var _a, _b;
1037
+ if (syncCleanup || typeof globalThis.addEventListener !== "function") return syncCleanup || (() => {
1038
+ });
1039
+ const refresh = () => {
1040
+ if (!globalThis.document || globalThis.document.visibilityState === "visible") {
1041
+ getPreferences({ force: true }).catch(() => {
1042
+ });
1043
+ }
1044
+ };
1045
+ const onVisibility = () => refresh();
1046
+ (_b = (_a = globalThis.document) == null ? void 0 : _a.addEventListener) == null ? void 0 : _b.call(_a, "visibilitychange", onVisibility);
1047
+ if (refreshOnFocus) globalThis.addEventListener("focus", refresh);
1048
+ syncCleanup = () => {
1049
+ var _a2, _b2;
1050
+ (_b2 = (_a2 = globalThis.document) == null ? void 0 : _a2.removeEventListener) == null ? void 0 : _b2.call(_a2, "visibilitychange", onVisibility);
1051
+ if (refreshOnFocus) globalThis.removeEventListener("focus", refresh);
1052
+ syncCleanup = null;
1053
+ };
1054
+ return syncCleanup;
1055
+ }
1056
+ function localeFor(language) {
1057
+ const supported = Intl.DateTimeFormat.supportedLocalesOf([language]);
1058
+ return supported[0] || "en";
1059
+ }
1060
+ function fixedOffset(value) {
1061
+ const match = /^UTC([+-])(\d{2}):(\d{2})$/.exec(value || "");
1062
+ if (!match) return null;
1063
+ const minutes = Number(match[2]) * 60 + Number(match[3]);
1064
+ return (match[1] === "-" ? -1 : 1) * minutes;
1065
+ }
1066
+ function formatUserDate(input, preferences2 = cachedPreferences || PREFERENCE_DEFAULTS, options = {}) {
1067
+ const value = normalizePreferences(preferences2);
1068
+ const date = input instanceof Date ? input : new Date(input);
1069
+ const offset = fixedOffset(value.timezone);
1070
+ const adjusted = offset === null ? date : new Date(date.getTime() + offset * 6e4);
1071
+ const timeZone = offset === null ? value.timezone : "UTC";
1072
+ const hasExplicitParts = ["year", "month", "day", "weekday", "hour", "minute", "second", "dateStyle", "timeStyle"].some((key) => options[key] !== void 0);
1073
+ const dateOptions = hasExplicitParts ? options : { year: "numeric", month: "2-digit", day: "2-digit" };
1074
+ const formatter = new Intl.DateTimeFormat(localeFor(value.language), {
1075
+ ...dateOptions,
1076
+ timeZone,
1077
+ ...dateOptions.hour || dateOptions.timeStyle ? { hour12: value.timeFormat === "12h" } : {}
1078
+ });
1079
+ if (!hasExplicitParts) {
1080
+ const parts = Object.fromEntries(formatter.formatToParts(adjusted).map((part) => [part.type, part.value]));
1081
+ if (value.dateFormat === "DD/MM/YYYY") return `${parts.day}/${parts.month}/${parts.year}`;
1082
+ if (value.dateFormat === "YYYY-MM-DD") return `${parts.year}-${parts.month}-${parts.day}`;
1083
+ return `${parts.month}/${parts.day}/${parts.year}`;
1084
+ }
1085
+ return formatter.format(adjusted);
1086
+ }
1087
+ function formatUserNumber(input, preferences2 = cachedPreferences || PREFERENCE_DEFAULTS, options = {}) {
1088
+ const value = normalizePreferences(preferences2);
1089
+ return new Intl.NumberFormat(localeFor(value.language), options).format(input);
1090
+ }
1091
+ function formatUserRelativeTime(input, preferences2 = cachedPreferences || PREFERENCE_DEFAULTS, now = /* @__PURE__ */ new Date()) {
1092
+ const value = normalizePreferences(preferences2);
1093
+ const deltaSeconds = (new Date(input).getTime() - now.getTime()) / 1e3;
1094
+ const units = [
1095
+ ["year", 31536e3],
1096
+ ["month", 2592e3],
1097
+ ["week", 604800],
1098
+ ["day", 86400],
1099
+ ["hour", 3600],
1100
+ ["minute", 60],
1101
+ ["second", 1]
1102
+ ];
1103
+ const [unit, seconds] = units.find(([, size]) => Math.abs(deltaSeconds) >= size) || units.at(-1);
1104
+ return new Intl.RelativeTimeFormat(localeFor(value.language), { numeric: "auto" }).format(Math.round(deltaSeconds / seconds), unit);
1105
+ }
1106
+ var preferences = {
1107
+ defaults: PREFERENCE_DEFAULTS,
1108
+ get: getPreferences,
1109
+ update: updatePreferences,
1110
+ subscribe: subscribePreferences,
1111
+ clearCache: clearPreferenceCache,
1112
+ apply: applyPreferences,
1113
+ startSync: startPreferenceSync,
1114
+ formatDate: formatUserDate,
1115
+ formatNumber: formatUserNumber,
1116
+ formatRelativeTime: formatUserRelativeTime,
1117
+ normalize: normalizePreferences,
1118
+ resolveTheme
1119
+ };
1120
+
812
1121
  // react/AuthProvider.jsx
813
1122
  import React, { createContext, useState, useEffect, useRef } from "react";
814
1123
  var AuthContext = createContext();
@@ -816,6 +1125,10 @@ var AuthContext = createContext();
816
1125
  // react/useAuth.js
817
1126
  import { useContext } from "react";
818
1127
 
1128
+ // react/PreferencesProvider.jsx
1129
+ import React2, { createContext as createContext2, useContext as useContext2, useEffect as useEffect2, useMemo, useState as useState2 } from "react";
1130
+ var PreferencesContext = createContext2(null);
1131
+
819
1132
  // index.js
820
1133
  var auth = {
821
1134
  // ๐Ÿ”ง Config
@@ -846,6 +1159,10 @@ var auth = {
846
1159
  // โœ… Debug function
847
1160
  // ๐ŸŒ Authenticated API client
848
1161
  api: api_default,
1162
+ preferences,
1163
+ // ๐Ÿ”Ž Safe authentication diagnostics
1164
+ getDiagnosticContext,
1165
+ emitAuthDiagnostic,
849
1166
  // ๐Ÿงช Utilities
850
1167
  decodeToken,
851
1168
  isTokenExpired,
@@ -948,7 +1265,7 @@ var useSessionMonitor = (options = {}) => {
948
1265
  staleTime: refetchInterval / 2
949
1266
  // Consider stale at half the interval
950
1267
  });
951
- useEffect2(() => {
1268
+ useEffect3(() => {
952
1269
  if (!enabled) return;
953
1270
  const handleVisibilityChange = () => {
954
1271
  if (document.visibilityState === "visible" && auth.getToken()) {
@@ -961,7 +1278,7 @@ var useSessionMonitor = (options = {}) => {
961
1278
  document.removeEventListener("visibilitychange", handleVisibilityChange);
962
1279
  };
963
1280
  }, [enabled, queryClient]);
964
- useEffect2(() => {
1281
+ useEffect3(() => {
965
1282
  if (validateOnMount && enabled && auth.getToken()) {
966
1283
  queryClient.invalidateQueries({ queryKey: ["session-validation"] });
967
1284
  }