@spidy092/auth-client 3.0.8 โ†’ 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";
@@ -173,6 +173,21 @@ function removeTokenListener(listener) {
173
173
  function getListenerCount() {
174
174
  return listeners.size;
175
175
  }
176
+ if (typeof window !== "undefined" && window.addEventListener) {
177
+ window.addEventListener("storage", (event) => {
178
+ if (event.key !== "authToken") return;
179
+ const previousToken = accessToken;
180
+ accessToken = event.newValue || null;
181
+ if (previousToken === accessToken) return;
182
+ listeners.forEach((listener) => {
183
+ try {
184
+ listener(accessToken, previousToken);
185
+ } catch (err) {
186
+ console.warn("Token listener error:", err);
187
+ }
188
+ });
189
+ });
190
+ }
176
191
 
177
192
  // config.js
178
193
  var config = {
@@ -217,8 +232,8 @@ function setConfig(customConfig = {}) {
217
232
  // โœ… Auto-detect router mode
218
233
  isRouter: customConfig.isRouter || customConfig.clientKey === "account-ui"
219
234
  };
235
+ enableRefreshTokenPersistence(config.persistRefreshToken);
220
236
  if (config.persistRefreshToken) {
221
- enableRefreshTokenPersistence(true);
222
237
  console.log("\u{1F4E6} Refresh token persistence ENABLED (localStorage on HTTPS)");
223
238
  }
224
239
  console.log(`\u{1F527} Auth Client Mode: ${config.isRouter ? "ROUTER" : "CLIENT"}`, {
@@ -234,6 +249,82 @@ function isRouterMode() {
234
249
  return config.isRouter;
235
250
  }
236
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
+
237
328
  // core.js
238
329
  var callbackProcessed = false;
239
330
  function login(clientKeyArg, redirectUriArg) {
@@ -246,14 +337,16 @@ function login(clientKeyArg, redirectUriArg) {
246
337
  } = getConfig();
247
338
  const clientKey = clientKeyArg || defaultClientKey;
248
339
  const redirectUri = redirectUriArg || defaultRedirectUri;
249
- console.log("\u{1F504} Smart Login initiated:", {
250
- mode: isRouterMode() ? "ROUTER" : "CLIENT",
251
- clientKey,
252
- redirectUri
253
- });
254
340
  if (!clientKey || !redirectUri) {
341
+ emitAuthDiagnostic("LOGIN_REJECTED", "FAILURE", "CLIENT_CONFIG_MISSING", { clientKey });
255
342
  throw new Error("Missing clientKey or redirectUri");
256
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 });
257
350
  sessionStorage.setItem("originalApp", clientKey);
258
351
  sessionStorage.setItem("returnUrl", redirectUri);
259
352
  if (isRouterMode()) {
@@ -268,13 +361,9 @@ function routerLogin(clientKey, redirectUri) {
268
361
  if (redirectUri) {
269
362
  params.append("redirect_uri", redirectUri);
270
363
  }
364
+ params.append("correlation_id", getDiagnosticContext().correlationId);
271
365
  const query = params.toString();
272
366
  const backendLoginUrl = `${authBaseUrl}/login/${clientKey}${query ? `?${query}` : ""}`;
273
- console.log("\u{1F3ED} Router Login: Direct backend authentication", {
274
- clientKey,
275
- redirectUri,
276
- backendUrl: backendLoginUrl
277
- });
278
367
  window.location.href = backendLoginUrl;
279
368
  }
280
369
  function clientLogin(clientKey, redirectUri) {
@@ -286,19 +375,19 @@ function clientLogin(clientKey, redirectUri) {
286
375
  params.append("redirect_uri", redirectUri);
287
376
  }
288
377
  const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;
289
- console.log("\u{1F504} Client Login: Redirecting to centralized login", {
290
- clientKey,
291
- redirectUri,
292
- centralizedUrl: centralizedLoginUrl
293
- });
294
378
  window.location.href = centralizedLoginUrl;
295
379
  }
296
- async function logout() {
380
+ async function logout(options = {}) {
297
381
  resetCallbackState();
298
382
  const { clientKey, authBaseUrl, accountUiUrl } = getConfig();
383
+ const scope = options.scope === "client" ? "client" : "sso";
299
384
  const token = getToken();
300
385
  const refreshToken2 = getRefreshToken();
301
- console.log("\u{1F6AA} Smart Logout initiated", { mode: isRouterMode() ? "ROUTER" : "CLIENT", clientKey });
386
+ console.log("\u{1F6AA} Smart Logout initiated", {
387
+ mode: isRouterMode() ? "ROUTER" : "CLIENT",
388
+ clientKey,
389
+ scope
390
+ });
302
391
  clearToken();
303
392
  clearRefreshToken();
304
393
  sessionStorage.removeItem("originalApp");
@@ -308,21 +397,33 @@ async function logout() {
308
397
  method: "POST",
309
398
  credentials: "include",
310
399
  headers: {
400
+ ...diagnosticHeaders(),
311
401
  "Authorization": token ? `Bearer ${token}` : "",
312
402
  "Content-Type": "application/json"
313
403
  },
314
- body: JSON.stringify({ refreshToken: refreshToken2 })
404
+ body: JSON.stringify({ refreshToken: refreshToken2, scope })
315
405
  });
406
+ if (!response.ok) {
407
+ throw new Error(`Logout failed: ${response.status}`);
408
+ }
316
409
  const data = await response.json();
317
410
  console.log("\u2705 Logout response:", data);
411
+ if (data == null ? void 0 : data.logoutRedirectUrl) {
412
+ window.location.replace(data.logoutRedirectUrl);
413
+ return;
414
+ }
318
415
  if (data == null ? void 0 : data.keycloakLogoutUrl) {
319
- window.location.href = data.keycloakLogoutUrl;
416
+ window.location.replace(data.keycloakLogoutUrl);
320
417
  return;
321
418
  }
322
419
  } catch (error) {
323
420
  console.warn("\u26A0\uFE0F Logout backend call failed:", error);
324
421
  }
325
- window.location.href = isRouterMode() ? "/login" : `${accountUiUrl}/login?client=${clientKey}&logout=true`;
422
+ const fallbackUrl = isRouterMode() ? new URL("/login", window.location.origin) : new URL("/login", accountUiUrl);
423
+ fallbackUrl.searchParams.set("logged_out", "true");
424
+ fallbackUrl.searchParams.set("client", clientKey);
425
+ fallbackUrl.searchParams.set("scope", scope);
426
+ window.location.replace(fallbackUrl.toString());
326
427
  }
327
428
  function handleCallback() {
328
429
  var _a;
@@ -342,11 +443,19 @@ function handleCallback() {
342
443
  callbackProcessed = false;
343
444
  }
344
445
  callbackProcessed = true;
446
+ clearLoginLock();
345
447
  sessionStorage.removeItem("originalApp");
346
448
  sessionStorage.removeItem("returnUrl");
347
449
  if (error) {
348
450
  const errorDescription = params.get("error_description") || error;
349
- 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;
350
459
  }
351
460
  if (accessToken2) {
352
461
  setToken(accessToken2);
@@ -369,9 +478,19 @@ function handleCallback() {
369
478
  url.searchParams.delete("error_description");
370
479
  window.history.replaceState({}, "", url);
371
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
+ });
372
485
  return accessToken2;
373
486
  }
374
- 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;
375
494
  }
376
495
  function resetCallbackState() {
377
496
  callbackProcessed = false;
@@ -386,7 +505,7 @@ async function refreshToken() {
386
505
  }
387
506
  refreshInProgress = true;
388
507
  refreshPromise = (async () => {
389
- var _a, _b, _c, _d;
508
+ var _a, _b, _c, _d, _e, _f;
390
509
  try {
391
510
  const storedRefreshToken = getRefreshToken();
392
511
  console.log("\u{1F504} Refreshing token:", {
@@ -399,6 +518,7 @@ async function refreshToken() {
399
518
  credentials: "include",
400
519
  // โœ… Include httpOnly cookies (for HTTPS)
401
520
  headers: {
521
+ ...diagnosticHeaders(),
402
522
  "Content-Type": "application/json"
403
523
  }
404
524
  };
@@ -409,8 +529,19 @@ async function refreshToken() {
409
529
  const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
410
530
  if (!response.ok) {
411
531
  const errorText = await response.text();
412
- console.error("\u274C Token refresh failed:", response.status, errorText);
413
- 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;
414
545
  }
415
546
  const data = await response.json();
416
547
  const { access_token, refresh_token: new_refresh_token } = data;
@@ -423,10 +554,11 @@ async function refreshToken() {
423
554
  console.log("\u{1F504} New refresh token stored from rotation");
424
555
  }
425
556
  console.log("\u2705 Token refresh successful, listeners notified");
557
+ emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
426
558
  return access_token;
427
559
  } catch (err) {
428
560
  console.error("\u274C Token refresh error:", err);
429
- 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"));
430
562
  if (isAuthRejection) {
431
563
  clearToken();
432
564
  clearRefreshToken();
@@ -449,6 +581,7 @@ async function validateCurrentSession() {
449
581
  const response = await fetch(`${authBaseUrl}/account/validate-session`, {
450
582
  method: "GET",
451
583
  headers: {
584
+ ...diagnosticHeaders(),
452
585
  "Authorization": `Bearer ${token}`,
453
586
  "Content-Type": "application/json"
454
587
  },
@@ -701,6 +834,7 @@ api.interceptors.request.use((config2) => {
701
834
  if ((runtimeConfig == null ? void 0 : runtimeConfig.clientKey) && !config2.headers["X-Client-Key"]) {
702
835
  config2.headers["X-Client-Key"] = runtimeConfig.clientKey;
703
836
  }
837
+ Object.assign(config2.headers, diagnosticHeaders());
704
838
  const token = getToken();
705
839
  if (token) {
706
840
  config2.headers.Authorization = `Bearer ${token}`;
@@ -719,6 +853,10 @@ api.interceptors.response.use(
719
853
  return Promise.reject(error);
720
854
  }
721
855
  config2._retry = true;
856
+ emitAuthDiagnostic("API_401_REFRESH_STARTED", "PENDING", "HTTP_401", {
857
+ clientKey: getConfig().clientKey,
858
+ status: 401
859
+ });
722
860
  if (!refreshPromise2) {
723
861
  refreshPromise2 = refreshToken().then((newToken) => {
724
862
  refreshPromise2 = null;
@@ -778,6 +916,208 @@ function isAuthenticated() {
778
916
  return !!token && !isTokenExpired(token, 10);
779
917
  }
780
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
+
781
1121
  // react/AuthProvider.jsx
782
1122
  import React, { createContext, useState, useEffect, useRef } from "react";
783
1123
  var AuthContext = createContext();
@@ -785,6 +1125,10 @@ var AuthContext = createContext();
785
1125
  // react/useAuth.js
786
1126
  import { useContext } from "react";
787
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
+
788
1132
  // index.js
789
1133
  var auth = {
790
1134
  // ๐Ÿ”ง Config
@@ -794,6 +1138,8 @@ var auth = {
794
1138
  // ๐Ÿ” Core flows
795
1139
  login,
796
1140
  logout,
1141
+ logoutClient: () => logout({ scope: "client" }),
1142
+ logoutSso: () => logout({ scope: "sso" }),
797
1143
  handleCallback,
798
1144
  refreshToken,
799
1145
  resetCallbackState,
@@ -813,6 +1159,10 @@ var auth = {
813
1159
  // โœ… Debug function
814
1160
  // ๐ŸŒ Authenticated API client
815
1161
  api: api_default,
1162
+ preferences,
1163
+ // ๐Ÿ”Ž Safe authentication diagnostics
1164
+ getDiagnosticContext,
1165
+ emitAuthDiagnostic,
816
1166
  // ๐Ÿงช Utilities
817
1167
  decodeToken,
818
1168
  isTokenExpired,
@@ -915,7 +1265,7 @@ var useSessionMonitor = (options = {}) => {
915
1265
  staleTime: refetchInterval / 2
916
1266
  // Consider stale at half the interval
917
1267
  });
918
- useEffect2(() => {
1268
+ useEffect3(() => {
919
1269
  if (!enabled) return;
920
1270
  const handleVisibilityChange = () => {
921
1271
  if (document.visibilityState === "visible" && auth.getToken()) {
@@ -928,7 +1278,7 @@ var useSessionMonitor = (options = {}) => {
928
1278
  document.removeEventListener("visibilitychange", handleVisibilityChange);
929
1279
  };
930
1280
  }, [enabled, queryClient]);
931
- useEffect2(() => {
1281
+ useEffect3(() => {
932
1282
  if (validateOnMount && enabled && auth.getToken()) {
933
1283
  queryClient.invalidateQueries({ queryKey: ["session-validation"] });
934
1284
  }