@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.
package/dist/index.js CHANGED
@@ -228,8 +228,8 @@ function setConfig(customConfig = {}) {
228
228
  // โœ… Auto-detect router mode
229
229
  isRouter: customConfig.isRouter || customConfig.clientKey === "account-ui"
230
230
  };
231
+ enableRefreshTokenPersistence(config.persistRefreshToken);
231
232
  if (config.persistRefreshToken) {
232
- enableRefreshTokenPersistence(true);
233
233
  console.log("\u{1F4E6} Refresh token persistence ENABLED (localStorage on HTTPS)");
234
234
  }
235
235
  console.log(`\u{1F527} Auth Client Mode: ${config.isRouter ? "ROUTER" : "CLIENT"}`, {
@@ -245,6 +245,82 @@ function isRouterMode() {
245
245
  return config.isRouter;
246
246
  }
247
247
 
248
+ // diagnostics.js
249
+ var STORAGE_KEY = "auth_diagnostic_context";
250
+ var LOGIN_LOCK_KEY = "auth_login_lock";
251
+ var randomId = () => {
252
+ var _a;
253
+ if ((_a = globalThis.crypto) == null ? void 0 : _a.randomUUID) return globalThis.crypto.randomUUID();
254
+ return `auth-${Date.now()}-${Math.random().toString(36).slice(2)}`;
255
+ };
256
+ var fingerprint = async (value) => {
257
+ var _a;
258
+ if (!value || !((_a = globalThis.crypto) == null ? void 0 : _a.subtle)) return null;
259
+ const bytes = new TextEncoder().encode(String(value));
260
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
261
+ return [...new Uint8Array(digest)].slice(0, 6).map((byte) => byte.toString(16).padStart(2, "0")).join("");
262
+ };
263
+ function getDiagnosticContext() {
264
+ try {
265
+ const existing = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || "null");
266
+ if (existing == null ? void 0 : existing.correlationId) return existing;
267
+ } catch {
268
+ }
269
+ const context = { correlationId: randomId(), startedAt: Date.now() };
270
+ try {
271
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(context));
272
+ } catch {
273
+ }
274
+ return context;
275
+ }
276
+ function resetDiagnosticContext() {
277
+ const context = { correlationId: randomId(), startedAt: Date.now() };
278
+ try {
279
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(context));
280
+ } catch {
281
+ }
282
+ return context;
283
+ }
284
+ function diagnosticHeaders() {
285
+ const { correlationId } = getDiagnosticContext();
286
+ return { "X-Correlation-ID": correlationId, "X-Request-ID": correlationId };
287
+ }
288
+ function acquireLoginLock(clientKey, redirectUri, ttlMs = 5e3) {
289
+ const now = Date.now();
290
+ const signature = `${clientKey}|${redirectUri}`;
291
+ try {
292
+ const current = JSON.parse(sessionStorage.getItem(LOGIN_LOCK_KEY) || "null");
293
+ if ((current == null ? void 0 : current.signature) === signature && now - current.createdAt < ttlMs) return false;
294
+ sessionStorage.setItem(LOGIN_LOCK_KEY, JSON.stringify({ signature, createdAt: now }));
295
+ } catch {
296
+ }
297
+ return true;
298
+ }
299
+ function clearLoginLock() {
300
+ try {
301
+ sessionStorage.removeItem(LOGIN_LOCK_KEY);
302
+ } catch {
303
+ }
304
+ }
305
+ async function emitAuthDiagnostic(event, outcome, reasonCode, details = {}) {
306
+ const context = getDiagnosticContext();
307
+ const safe = {
308
+ eventType: "auth_client_diagnostic",
309
+ event,
310
+ outcome,
311
+ reasonCode,
312
+ correlationId: context.correlationId,
313
+ elapsedMs: Date.now() - context.startedAt,
314
+ clientKey: details.clientKey || null,
315
+ status: details.status || null,
316
+ stateFingerprint: await fingerprint(details.state),
317
+ online: typeof navigator === "undefined" ? null : navigator.onLine
318
+ };
319
+ const method = outcome === "FAILURE" ? "error" : outcome === "WARNING" ? "warn" : "info";
320
+ console[method]("[auth-client]", safe);
321
+ return safe;
322
+ }
323
+
248
324
  // core.js
249
325
  var callbackProcessed = false;
250
326
  function login(clientKeyArg, redirectUriArg) {
@@ -257,14 +333,16 @@ function login(clientKeyArg, redirectUriArg) {
257
333
  } = getConfig();
258
334
  const clientKey = clientKeyArg || defaultClientKey;
259
335
  const redirectUri = redirectUriArg || defaultRedirectUri;
260
- console.log("\u{1F504} Smart Login initiated:", {
261
- mode: isRouterMode() ? "ROUTER" : "CLIENT",
262
- clientKey,
263
- redirectUri
264
- });
265
336
  if (!clientKey || !redirectUri) {
337
+ emitAuthDiagnostic("LOGIN_REJECTED", "FAILURE", "CLIENT_CONFIG_MISSING", { clientKey });
266
338
  throw new Error("Missing clientKey or redirectUri");
267
339
  }
340
+ if (!acquireLoginLock(clientKey, redirectUri)) {
341
+ emitAuthDiagnostic("LOGIN_DUPLICATE_SUPPRESSED", "WARNING", "LOGIN_ALREADY_IN_PROGRESS", { clientKey });
342
+ return false;
343
+ }
344
+ resetDiagnosticContext();
345
+ emitAuthDiagnostic("LOGIN_INITIATED", "PENDING", "NONE", { clientKey });
268
346
  sessionStorage.setItem("originalApp", clientKey);
269
347
  sessionStorage.setItem("returnUrl", redirectUri);
270
348
  if (isRouterMode()) {
@@ -279,13 +357,9 @@ function routerLogin(clientKey, redirectUri) {
279
357
  if (redirectUri) {
280
358
  params.append("redirect_uri", redirectUri);
281
359
  }
360
+ params.append("correlation_id", getDiagnosticContext().correlationId);
282
361
  const query = params.toString();
283
362
  const backendLoginUrl = `${authBaseUrl}/login/${clientKey}${query ? `?${query}` : ""}`;
284
- console.log("\u{1F3ED} Router Login: Direct backend authentication", {
285
- clientKey,
286
- redirectUri,
287
- backendUrl: backendLoginUrl
288
- });
289
363
  window.location.href = backendLoginUrl;
290
364
  }
291
365
  function clientLogin(clientKey, redirectUri) {
@@ -297,11 +371,6 @@ function clientLogin(clientKey, redirectUri) {
297
371
  params.append("redirect_uri", redirectUri);
298
372
  }
299
373
  const centralizedLoginUrl = `${accountUiUrl}/login?${params.toString()}`;
300
- console.log("\u{1F504} Client Login: Redirecting to centralized login", {
301
- clientKey,
302
- redirectUri,
303
- centralizedUrl: centralizedLoginUrl
304
- });
305
374
  window.location.href = centralizedLoginUrl;
306
375
  }
307
376
  async function logout(options = {}) {
@@ -324,6 +393,7 @@ async function logout(options = {}) {
324
393
  method: "POST",
325
394
  credentials: "include",
326
395
  headers: {
396
+ ...diagnosticHeaders(),
327
397
  "Authorization": token ? `Bearer ${token}` : "",
328
398
  "Content-Type": "application/json"
329
399
  },
@@ -369,11 +439,19 @@ function handleCallback() {
369
439
  callbackProcessed = false;
370
440
  }
371
441
  callbackProcessed = true;
442
+ clearLoginLock();
372
443
  sessionStorage.removeItem("originalApp");
373
444
  sessionStorage.removeItem("returnUrl");
374
445
  if (error) {
375
446
  const errorDescription = params.get("error_description") || error;
376
- throw new Error(`Authentication failed: ${errorDescription}`);
447
+ const authError = new Error(`Authentication failed: ${errorDescription}`);
448
+ authError.code = error;
449
+ authError.correlationId = getDiagnosticContext().correlationId;
450
+ emitAuthDiagnostic("CALLBACK_REJECTED", "FAILURE", error.toUpperCase(), {
451
+ clientKey: getConfig().clientKey,
452
+ state: params.get("state")
453
+ });
454
+ throw authError;
377
455
  }
378
456
  if (accessToken2) {
379
457
  setToken(accessToken2);
@@ -396,9 +474,19 @@ function handleCallback() {
396
474
  url.searchParams.delete("error_description");
397
475
  window.history.replaceState({}, "", url);
398
476
  console.log("\u2705 Callback processed successfully, token stored");
477
+ emitAuthDiagnostic("CALLBACK_COMPLETED", "SUCCESS", "NONE", {
478
+ clientKey: getConfig().clientKey,
479
+ state: params.get("state")
480
+ });
399
481
  return accessToken2;
400
482
  }
401
- throw new Error("No access token found in callback URL");
483
+ emitAuthDiagnostic("CALLBACK_REJECTED", "FAILURE", "ACCESS_TOKEN_MISSING", {
484
+ clientKey: getConfig().clientKey,
485
+ state: params.get("state")
486
+ });
487
+ const missingTokenError = new Error("No access token found in callback URL");
488
+ missingTokenError.code = "ACCESS_TOKEN_MISSING";
489
+ throw missingTokenError;
402
490
  }
403
491
  function resetCallbackState() {
404
492
  callbackProcessed = false;
@@ -413,7 +501,7 @@ async function refreshToken() {
413
501
  }
414
502
  refreshInProgress = true;
415
503
  refreshPromise = (async () => {
416
- var _a, _b, _c, _d;
504
+ var _a, _b, _c, _d, _e, _f;
417
505
  try {
418
506
  const storedRefreshToken = getRefreshToken();
419
507
  console.log("\u{1F504} Refreshing token:", {
@@ -426,6 +514,7 @@ async function refreshToken() {
426
514
  credentials: "include",
427
515
  // โœ… Include httpOnly cookies (for HTTPS)
428
516
  headers: {
517
+ ...diagnosticHeaders(),
429
518
  "Content-Type": "application/json"
430
519
  }
431
520
  };
@@ -436,8 +525,19 @@ async function refreshToken() {
436
525
  const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, requestOptions);
437
526
  if (!response.ok) {
438
527
  const errorText = await response.text();
439
- console.error("\u274C Token refresh failed:", response.status, errorText);
440
- throw new Error(`Refresh failed: ${response.status}`);
528
+ let serverCode = null;
529
+ try {
530
+ serverCode = ((_a = JSON.parse(errorText)) == null ? void 0 : _a.error) || ((_b = JSON.parse(errorText)) == null ? void 0 : _b.code);
531
+ } catch {
532
+ }
533
+ emitAuthDiagnostic("TOKEN_REFRESH_REJECTED", "FAILURE", serverCode || `HTTP_${response.status}`, {
534
+ clientKey,
535
+ status: response.status
536
+ });
537
+ const refreshError = new Error(`Refresh failed: ${response.status}`);
538
+ refreshError.code = serverCode || `HTTP_${response.status}`;
539
+ refreshError.status = response.status;
540
+ throw refreshError;
441
541
  }
442
542
  const data = await response.json();
443
543
  const { access_token, refresh_token: new_refresh_token } = data;
@@ -450,10 +550,11 @@ async function refreshToken() {
450
550
  console.log("\u{1F504} New refresh token stored from rotation");
451
551
  }
452
552
  console.log("\u2705 Token refresh successful, listeners notified");
553
+ emitAuthDiagnostic("TOKEN_REFRESH_COMPLETED", "SUCCESS", "NONE", { clientKey });
453
554
  return access_token;
454
555
  } catch (err) {
455
556
  console.error("\u274C Token refresh error:", err);
456
- 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"));
557
+ 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"));
457
558
  if (isAuthRejection) {
458
559
  clearToken();
459
560
  clearRefreshToken();
@@ -476,6 +577,7 @@ async function validateCurrentSession() {
476
577
  const response = await fetch(`${authBaseUrl}/account/validate-session`, {
477
578
  method: "GET",
478
579
  headers: {
580
+ ...diagnosticHeaders(),
479
581
  "Authorization": `Bearer ${token}`,
480
582
  "Content-Type": "application/json"
481
583
  },
@@ -728,6 +830,7 @@ api.interceptors.request.use((config2) => {
728
830
  if ((runtimeConfig == null ? void 0 : runtimeConfig.clientKey) && !config2.headers["X-Client-Key"]) {
729
831
  config2.headers["X-Client-Key"] = runtimeConfig.clientKey;
730
832
  }
833
+ Object.assign(config2.headers, diagnosticHeaders());
731
834
  const token = getToken();
732
835
  if (token) {
733
836
  config2.headers.Authorization = `Bearer ${token}`;
@@ -746,6 +849,10 @@ api.interceptors.response.use(
746
849
  return Promise.reject(error);
747
850
  }
748
851
  config2._retry = true;
852
+ emitAuthDiagnostic("API_401_REFRESH_STARTED", "PENDING", "HTTP_401", {
853
+ clientKey: getConfig().clientKey,
854
+ status: 401
855
+ });
749
856
  if (!refreshPromise2) {
750
857
  refreshPromise2 = refreshToken().then((newToken) => {
751
858
  refreshPromise2 = null;
@@ -805,6 +912,208 @@ function isAuthenticated() {
805
912
  return !!token && !isTokenExpired(token, 10);
806
913
  }
807
914
 
915
+ // preferences.js
916
+ var PREFERENCE_DEFAULTS = Object.freeze({
917
+ theme: "system",
918
+ colorScheme: "blue",
919
+ density: "comfortable",
920
+ reducedMotion: false,
921
+ language: "en",
922
+ timezone: "Etc/UTC",
923
+ dateFormat: "MM/DD/YYYY",
924
+ timeFormat: "12h",
925
+ highContrast: false,
926
+ largeText: false
927
+ });
928
+ var ALLOWED = {
929
+ theme: /* @__PURE__ */ new Set(["light", "dark", "system"]),
930
+ density: /* @__PURE__ */ new Set(["compact", "comfortable", "spacious"]),
931
+ timeFormat: /* @__PURE__ */ new Set(["12h", "24h"])
932
+ };
933
+ var listeners2 = /* @__PURE__ */ new Set();
934
+ var cachedPreferences = null;
935
+ var pendingRequest = null;
936
+ var syncCleanup = null;
937
+ function extractResponseData(response) {
938
+ var _a;
939
+ return ((_a = response == null ? void 0 : response.data) == null ? void 0 : _a.data) ?? (response == null ? void 0 : response.data) ?? response ?? {};
940
+ }
941
+ function normalizePreferences(value = {}) {
942
+ const next = { ...PREFERENCE_DEFAULTS };
943
+ for (const key of Object.keys(PREFERENCE_DEFAULTS)) {
944
+ if (!Object.hasOwn(value, key)) continue;
945
+ const candidate = value[key];
946
+ if (ALLOWED[key] && !ALLOWED[key].has(candidate)) continue;
947
+ if (typeof PREFERENCE_DEFAULTS[key] !== typeof candidate) continue;
948
+ next[key] = candidate;
949
+ }
950
+ return next;
951
+ }
952
+ function resolveTheme(theme, matchMedia2 = globalThis.matchMedia) {
953
+ if (theme === "dark" || theme === "light") return theme;
954
+ return typeof matchMedia2 === "function" && matchMedia2("(prefers-color-scheme: dark)").matches ? "dark" : "light";
955
+ }
956
+ function announce(preferences2) {
957
+ cachedPreferences = normalizePreferences(preferences2);
958
+ listeners2.forEach((listener) => listener(cachedPreferences));
959
+ return cachedPreferences;
960
+ }
961
+ async function getPreferences({ force = false } = {}) {
962
+ if (cachedPreferences && !force) return cachedPreferences;
963
+ if (pendingRequest) return pendingRequest;
964
+ pendingRequest = api_default.get("/account/preferences").then((response) => announce(extractResponseData(response))).finally(() => {
965
+ pendingRequest = null;
966
+ });
967
+ return pendingRequest;
968
+ }
969
+ async function updatePreferences(patch) {
970
+ const allowedPatch = {};
971
+ for (const key of Object.keys(PREFERENCE_DEFAULTS)) {
972
+ if (Object.hasOwn(patch, key)) allowedPatch[key] = patch[key];
973
+ }
974
+ const candidate = normalizePreferences({ ...cachedPreferences || PREFERENCE_DEFAULTS, ...allowedPatch });
975
+ const response = await api_default.put("/account/preferences", allowedPatch);
976
+ const saved = extractResponseData(response);
977
+ return announce(Object.keys(saved).length ? saved : candidate);
978
+ }
979
+ function subscribePreferences(listener) {
980
+ listeners2.add(listener);
981
+ if (cachedPreferences) listener(cachedPreferences);
982
+ return () => listeners2.delete(listener);
983
+ }
984
+ function clearPreferenceCache() {
985
+ cachedPreferences = null;
986
+ pendingRequest = null;
987
+ }
988
+ function applyPreferences(preferences2, documentRef = globalThis.document) {
989
+ var _a, _b, _c;
990
+ if (!(documentRef == null ? void 0 : documentRef.documentElement)) return;
991
+ const value = normalizePreferences(preferences2);
992
+ const root = documentRef.documentElement;
993
+ const resolvedTheme = resolveTheme(value.theme, (_b = (_a = documentRef.defaultView) == null ? void 0 : _a.matchMedia) == null ? void 0 : _b.bind(documentRef.defaultView));
994
+ root.dataset.theme = resolvedTheme;
995
+ root.dataset.themePreference = value.theme;
996
+ root.dataset.colorScheme = value.colorScheme;
997
+ root.dataset.density = value.density;
998
+ root.lang = value.language;
999
+ root.style.colorScheme = resolvedTheme;
1000
+ root.style.setProperty("--user-accent-color", {
1001
+ blue: "#1976d2",
1002
+ green: "#2e7d32",
1003
+ purple: "#7b1fa2",
1004
+ orange: "#c65d00",
1005
+ red: "#c62828"
1006
+ }[value.colorScheme] || "#1976d2");
1007
+ root.classList.toggle("user-reduced-motion", value.reducedMotion);
1008
+ root.classList.toggle("user-high-contrast", value.highContrast);
1009
+ root.classList.toggle("user-large-text", value.largeText);
1010
+ const styleId = "sso-user-preference-styles";
1011
+ if (!documentRef.getElementById(styleId)) {
1012
+ const style = documentRef.createElement("style");
1013
+ style.id = styleId;
1014
+ style.textContent = `
1015
+ html.user-large-text { font-size: 112.5%; }
1016
+ html.user-high-contrast { filter: contrast(1.15); }
1017
+ html[data-density="compact"] { --user-density-padding: 6px; }
1018
+ html[data-density="comfortable"] { --user-density-padding: 10px; }
1019
+ html[data-density="spacious"] { --user-density-padding: 14px; }
1020
+ html[data-density] .MuiListItem-root { padding-top: var(--user-density-padding); padding-bottom: var(--user-density-padding); }
1021
+ html.user-reduced-motion *, html.user-reduced-motion *::before, html.user-reduced-motion *::after {
1022
+ animation-duration: 0.01ms !important;
1023
+ animation-iteration-count: 1 !important;
1024
+ scroll-behavior: auto !important;
1025
+ transition-duration: 0.01ms !important;
1026
+ }
1027
+ `;
1028
+ (_c = documentRef.head) == null ? void 0 : _c.appendChild(style);
1029
+ }
1030
+ }
1031
+ function startPreferenceSync({ refreshOnFocus = true } = {}) {
1032
+ var _a, _b;
1033
+ if (syncCleanup || typeof globalThis.addEventListener !== "function") return syncCleanup || (() => {
1034
+ });
1035
+ const refresh = () => {
1036
+ if (!globalThis.document || globalThis.document.visibilityState === "visible") {
1037
+ getPreferences({ force: true }).catch(() => {
1038
+ });
1039
+ }
1040
+ };
1041
+ const onVisibility = () => refresh();
1042
+ (_b = (_a = globalThis.document) == null ? void 0 : _a.addEventListener) == null ? void 0 : _b.call(_a, "visibilitychange", onVisibility);
1043
+ if (refreshOnFocus) globalThis.addEventListener("focus", refresh);
1044
+ syncCleanup = () => {
1045
+ var _a2, _b2;
1046
+ (_b2 = (_a2 = globalThis.document) == null ? void 0 : _a2.removeEventListener) == null ? void 0 : _b2.call(_a2, "visibilitychange", onVisibility);
1047
+ if (refreshOnFocus) globalThis.removeEventListener("focus", refresh);
1048
+ syncCleanup = null;
1049
+ };
1050
+ return syncCleanup;
1051
+ }
1052
+ function localeFor(language) {
1053
+ const supported = Intl.DateTimeFormat.supportedLocalesOf([language]);
1054
+ return supported[0] || "en";
1055
+ }
1056
+ function fixedOffset(value) {
1057
+ const match = /^UTC([+-])(\d{2}):(\d{2})$/.exec(value || "");
1058
+ if (!match) return null;
1059
+ const minutes = Number(match[2]) * 60 + Number(match[3]);
1060
+ return (match[1] === "-" ? -1 : 1) * minutes;
1061
+ }
1062
+ function formatUserDate(input, preferences2 = cachedPreferences || PREFERENCE_DEFAULTS, options = {}) {
1063
+ const value = normalizePreferences(preferences2);
1064
+ const date = input instanceof Date ? input : new Date(input);
1065
+ const offset = fixedOffset(value.timezone);
1066
+ const adjusted = offset === null ? date : new Date(date.getTime() + offset * 6e4);
1067
+ const timeZone = offset === null ? value.timezone : "UTC";
1068
+ const hasExplicitParts = ["year", "month", "day", "weekday", "hour", "minute", "second", "dateStyle", "timeStyle"].some((key) => options[key] !== void 0);
1069
+ const dateOptions = hasExplicitParts ? options : { year: "numeric", month: "2-digit", day: "2-digit" };
1070
+ const formatter = new Intl.DateTimeFormat(localeFor(value.language), {
1071
+ ...dateOptions,
1072
+ timeZone,
1073
+ ...dateOptions.hour || dateOptions.timeStyle ? { hour12: value.timeFormat === "12h" } : {}
1074
+ });
1075
+ if (!hasExplicitParts) {
1076
+ const parts = Object.fromEntries(formatter.formatToParts(adjusted).map((part) => [part.type, part.value]));
1077
+ if (value.dateFormat === "DD/MM/YYYY") return `${parts.day}/${parts.month}/${parts.year}`;
1078
+ if (value.dateFormat === "YYYY-MM-DD") return `${parts.year}-${parts.month}-${parts.day}`;
1079
+ return `${parts.month}/${parts.day}/${parts.year}`;
1080
+ }
1081
+ return formatter.format(adjusted);
1082
+ }
1083
+ function formatUserNumber(input, preferences2 = cachedPreferences || PREFERENCE_DEFAULTS, options = {}) {
1084
+ const value = normalizePreferences(preferences2);
1085
+ return new Intl.NumberFormat(localeFor(value.language), options).format(input);
1086
+ }
1087
+ function formatUserRelativeTime(input, preferences2 = cachedPreferences || PREFERENCE_DEFAULTS, now = /* @__PURE__ */ new Date()) {
1088
+ const value = normalizePreferences(preferences2);
1089
+ const deltaSeconds = (new Date(input).getTime() - now.getTime()) / 1e3;
1090
+ const units = [
1091
+ ["year", 31536e3],
1092
+ ["month", 2592e3],
1093
+ ["week", 604800],
1094
+ ["day", 86400],
1095
+ ["hour", 3600],
1096
+ ["minute", 60],
1097
+ ["second", 1]
1098
+ ];
1099
+ const [unit, seconds] = units.find(([, size]) => Math.abs(deltaSeconds) >= size) || units.at(-1);
1100
+ return new Intl.RelativeTimeFormat(localeFor(value.language), { numeric: "auto" }).format(Math.round(deltaSeconds / seconds), unit);
1101
+ }
1102
+ var preferences = {
1103
+ defaults: PREFERENCE_DEFAULTS,
1104
+ get: getPreferences,
1105
+ update: updatePreferences,
1106
+ subscribe: subscribePreferences,
1107
+ clearCache: clearPreferenceCache,
1108
+ apply: applyPreferences,
1109
+ startSync: startPreferenceSync,
1110
+ formatDate: formatUserDate,
1111
+ formatNumber: formatUserNumber,
1112
+ formatRelativeTime: formatUserRelativeTime,
1113
+ normalize: normalizePreferences,
1114
+ resolveTheme
1115
+ };
1116
+
808
1117
  // react/AuthProvider.jsx
809
1118
  import React, { createContext, useState, useEffect, useRef } from "react";
810
1119
  var AuthContext = createContext();
@@ -868,7 +1177,8 @@ function AuthProvider({ children, onSessionExpired }) {
868
1177
  console.log("\u{1F4E5} Profile response status:", res.status);
869
1178
  if (!res.ok) throw new Error("Failed to fetch user");
870
1179
  return res.json();
871
- }).then((userData) => {
1180
+ }).then((responseBody) => {
1181
+ const userData = (responseBody == null ? void 0 : responseBody.data) ?? responseBody;
872
1182
  console.log("\u2705 Profile fetched successfully:", userData.email);
873
1183
  setUser(userData);
874
1184
  setSessionValid(true);
@@ -1015,6 +1325,62 @@ var useSessionMonitor = (options = {}) => {
1015
1325
  };
1016
1326
  };
1017
1327
 
1328
+ // react/PreferencesProvider.jsx
1329
+ import React2, { createContext as createContext2, useContext as useContext2, useEffect as useEffect3, useMemo, useState as useState2 } from "react";
1330
+ var PreferencesContext = createContext2(null);
1331
+ function PreferencesProvider({ children, initialPreferences, applyToDocument = true }) {
1332
+ const [preferences2, setPreferences] = useState2(() => ({
1333
+ ...PREFERENCE_DEFAULTS,
1334
+ ...initialPreferences
1335
+ }));
1336
+ const [loading, setLoading] = useState2(true);
1337
+ const [error, setError] = useState2(null);
1338
+ useEffect3(() => {
1339
+ const unsubscribe = subscribePreferences((next) => {
1340
+ setPreferences(next);
1341
+ setLoading(false);
1342
+ setError(null);
1343
+ });
1344
+ const stopSync = startPreferenceSync();
1345
+ getPreferences().catch((nextError) => {
1346
+ setError(nextError);
1347
+ setLoading(false);
1348
+ });
1349
+ return () => {
1350
+ unsubscribe();
1351
+ stopSync();
1352
+ };
1353
+ }, []);
1354
+ useEffect3(() => {
1355
+ if (applyToDocument) applyPreferences(preferences2);
1356
+ }, [applyToDocument, preferences2]);
1357
+ useEffect3(() => {
1358
+ var _a;
1359
+ if (preferences2.theme !== "system" || typeof matchMedia !== "function") return void 0;
1360
+ const media = matchMedia("(prefers-color-scheme: dark)");
1361
+ const applySystemTheme = () => applyPreferences(preferences2);
1362
+ (_a = media.addEventListener) == null ? void 0 : _a.call(media, "change", applySystemTheme);
1363
+ return () => {
1364
+ var _a2;
1365
+ return (_a2 = media.removeEventListener) == null ? void 0 : _a2.call(media, "change", applySystemTheme);
1366
+ };
1367
+ }, [preferences2]);
1368
+ const value = useMemo(() => ({
1369
+ preferences: preferences2,
1370
+ resolvedTheme: resolveTheme(preferences2.theme),
1371
+ loading,
1372
+ error,
1373
+ refresh: () => getPreferences({ force: true }),
1374
+ update: updatePreferences
1375
+ }), [error, loading, preferences2]);
1376
+ return /* @__PURE__ */ React2.createElement(PreferencesContext.Provider, { value }, children);
1377
+ }
1378
+ function usePreferences() {
1379
+ const context = useContext2(PreferencesContext);
1380
+ if (!context) throw new Error("usePreferences must be used within a PreferencesProvider");
1381
+ return context;
1382
+ }
1383
+
1018
1384
  // index.js
1019
1385
  var auth = {
1020
1386
  // ๐Ÿ”ง Config
@@ -1045,6 +1411,10 @@ var auth = {
1045
1411
  // โœ… Debug function
1046
1412
  // ๐ŸŒ Authenticated API client
1047
1413
  api: api_default,
1414
+ preferences,
1415
+ // ๐Ÿ”Ž Safe authentication diagnostics
1416
+ getDiagnosticContext,
1417
+ emitAuthDiagnostic,
1048
1418
  // ๐Ÿงช Utilities
1049
1419
  decodeToken,
1050
1420
  isTokenExpired,
@@ -1091,8 +1461,21 @@ var auth = {
1091
1461
  };
1092
1462
  export {
1093
1463
  AuthProvider,
1464
+ PREFERENCE_DEFAULTS,
1465
+ PreferencesProvider,
1466
+ applyPreferences,
1094
1467
  auth,
1468
+ formatUserDate,
1469
+ formatUserNumber,
1470
+ formatUserRelativeTime,
1471
+ getPreferences,
1472
+ normalizePreferences,
1473
+ resolveTheme,
1474
+ startPreferenceSync,
1475
+ subscribePreferences,
1476
+ updatePreferences,
1095
1477
  useAuth,
1478
+ usePreferences,
1096
1479
  useSessionMonitor
1097
1480
  };
1098
1481
  //# sourceMappingURL=index.js.map