antaeus.keycloak.react 2.3.0 → 2.5.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.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { createContext, useState, useRef, useCallback, useEffect, useMemo, useContext } from 'react';
2
- import { useAuth as useAuth$1, AuthProvider } from 'react-oidc-context';
1
+ import { createContext, useContext, useState, useRef, useCallback, useEffect, useMemo } from 'react';
2
+ import { useAuth as useAuth$1, AuthContext, AuthProvider } from 'react-oidc-context';
3
3
  import { WebStorageStateStore, User } from 'oidc-client-ts';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
  import { useLocation } from 'react-router-dom';
@@ -149,6 +149,14 @@ var useAuth = useAuth$1;
149
149
 
150
150
  // src/hooks/useTokenLifecycle.ts
151
151
  var STORAGE_KEY_PREFIX = "keycloak_first_login_";
152
+ var isNavigatorOffline = () => typeof navigator !== "undefined" && "onLine" in navigator && navigator.onLine === false;
153
+ var isTransientNetworkError = (error) => {
154
+ const message = error.message?.toLowerCase?.() ?? "";
155
+ if (!message) {
156
+ return false;
157
+ }
158
+ return message.includes("network") || message.includes("failed to fetch") || message.includes("fetch") || message.includes("timeout") || message.includes("temporarily unavailable") || message.includes("dns") || message.includes("offline");
159
+ };
152
160
  function useTokenLifecycle(options) {
153
161
  const {
154
162
  config,
@@ -163,6 +171,11 @@ function useTokenLifecycle(options) {
163
171
  const [isRefreshTokenExpired, setIsRefreshTokenExpired] = useState(false);
164
172
  const hasCheckedOnStartup = useRef(false);
165
173
  const supportsRefreshTokens = config.silentRenew?.useRefreshToken !== false;
174
+ const [pendingRetry, setPendingRetry] = useState(null);
175
+ const deferredRefreshReasonRef = useRef(null);
176
+ const enqueueRetry = useCallback((reason) => {
177
+ setPendingRetry((current) => current === reason ? current : reason);
178
+ }, []);
166
179
  const log = useCallback(
167
180
  (message, ...args) => {
168
181
  if (debug) {
@@ -213,6 +226,7 @@ function useTokenLifecycle(options) {
213
226
  return;
214
227
  }
215
228
  setIsRefreshing(true);
229
+ deferredRefreshReasonRef.current = null;
216
230
  try {
217
231
  if (checkRefreshTokenLifetime()) {
218
232
  log("Refresh token exceeded max lifetime, cannot refresh");
@@ -220,6 +234,12 @@ function useTokenLifecycle(options) {
220
234
  onRefreshTokenExpired?.();
221
235
  return;
222
236
  }
237
+ if (isNavigatorOffline()) {
238
+ log("Device offline, deferring token refresh until connection is restored");
239
+ deferredRefreshReasonRef.current = "offline";
240
+ enqueueRetry("offline");
241
+ return;
242
+ }
223
243
  onRefreshStart?.();
224
244
  log("Starting token refresh");
225
245
  await auth.signinSilent();
@@ -227,6 +247,21 @@ function useTokenLifecycle(options) {
227
247
  onRefreshSuccess?.();
228
248
  } catch (error) {
229
249
  const err = error instanceof Error ? error : new Error(String(error));
250
+ if (isTransientNetworkError(err)) {
251
+ const reason = isNavigatorOffline() ? "offline" : "network";
252
+ deferredRefreshReasonRef.current = reason;
253
+ if (reason === "offline") {
254
+ log("Token refresh deferred because device is offline");
255
+ } else {
256
+ log(
257
+ "Token refresh failed due to transient network issue, will retry automatically:",
258
+ err.message
259
+ );
260
+ }
261
+ enqueueRetry(reason);
262
+ onRefreshError?.(err);
263
+ return;
264
+ }
230
265
  log("Token refresh failed:", err.message);
231
266
  onRefreshError?.(err);
232
267
  throw err;
@@ -241,6 +276,7 @@ function useTokenLifecycle(options) {
241
276
  onRefreshSuccess,
242
277
  onRefreshError,
243
278
  onRefreshTokenExpired,
279
+ enqueueRetry,
244
280
  log
245
281
  ]);
246
282
  const checkAndRefreshTokens = useCallback(async () => {
@@ -279,6 +315,14 @@ function useTokenLifecycle(options) {
279
315
  if (timeUntilExpiry <= threshold) {
280
316
  try {
281
317
  await refreshToken();
318
+ if (deferredRefreshReasonRef.current) {
319
+ log(
320
+ "Token refresh was deferred due to pending retry:",
321
+ deferredRefreshReasonRef.current
322
+ );
323
+ deferredRefreshReasonRef.current = null;
324
+ return false;
325
+ }
282
326
  return true;
283
327
  } catch (error) {
284
328
  log("Failed to refresh tokens:", error);
@@ -296,6 +340,55 @@ function useTokenLifecycle(options) {
296
340
  getStorageKey,
297
341
  log
298
342
  ]);
343
+ useEffect(() => {
344
+ if (pendingRetry === null) {
345
+ return;
346
+ }
347
+ if (typeof window === "undefined") {
348
+ return;
349
+ }
350
+ let cancelled = false;
351
+ const executeRetry = () => {
352
+ if (cancelled) {
353
+ return;
354
+ }
355
+ if (isRefreshing) {
356
+ log("Skipping scheduled token refresh retry because a refresh is already in progress");
357
+ return;
358
+ }
359
+ refreshToken().catch((err) => {
360
+ log("Scheduled token refresh retry failed:", err);
361
+ });
362
+ };
363
+ if (pendingRetry === "offline") {
364
+ log("Waiting for online event to retry token refresh");
365
+ const handleOnline = () => {
366
+ if (cancelled) {
367
+ return;
368
+ }
369
+ log("Online event detected, retrying deferred token refresh");
370
+ setPendingRetry(null);
371
+ executeRetry();
372
+ };
373
+ window.addEventListener("online", handleOnline, { once: true });
374
+ return () => {
375
+ cancelled = true;
376
+ window.removeEventListener("online", handleOnline);
377
+ };
378
+ }
379
+ const timeoutId = window.setTimeout(() => {
380
+ if (cancelled) {
381
+ return;
382
+ }
383
+ log("Retrying token refresh after transient network failure delay");
384
+ setPendingRetry(null);
385
+ executeRetry();
386
+ }, 5e3);
387
+ return () => {
388
+ cancelled = true;
389
+ window.clearTimeout(timeoutId);
390
+ };
391
+ }, [pendingRetry, refreshToken, isRefreshing, log]);
299
392
  useEffect(() => {
300
393
  const strategy = config.silentRenew?.refreshStrategy || "both";
301
394
  const shouldCheckOnStartup = strategy === "startup" || strategy === "both";
@@ -656,6 +749,71 @@ var SessionMonitor = ({
656
749
  ] });
657
750
  };
658
751
 
752
+ // src/utils/tokenBridge.ts
753
+ var TokenBridge = class {
754
+ constructor() {
755
+ this.currentToken = null;
756
+ this.listeners = /* @__PURE__ */ new Set();
757
+ }
758
+ /**
759
+ * Update the tracked token and notify subscribers when it changes.
760
+ */
761
+ setToken(token) {
762
+ if (this.currentToken === token) {
763
+ return;
764
+ }
765
+ this.currentToken = token;
766
+ for (const listener of this.listeners) {
767
+ try {
768
+ listener(token);
769
+ } catch (error) {
770
+ console.error("[TokenBridge] Listener threw error:", error);
771
+ }
772
+ }
773
+ }
774
+ /**
775
+ * Read the most recent token value.
776
+ */
777
+ getToken() {
778
+ return this.currentToken;
779
+ }
780
+ /**
781
+ * Convenience helper for async callers.
782
+ */
783
+ async getTokenAsync() {
784
+ return this.currentToken;
785
+ }
786
+ /**
787
+ * Subscribe to token changes. Returns an unsubscribe callback.
788
+ */
789
+ subscribe(listener) {
790
+ this.listeners.add(listener);
791
+ listener(this.currentToken);
792
+ return () => {
793
+ this.listeners.delete(listener);
794
+ };
795
+ }
796
+ };
797
+ var createTokenBridge = () => new TokenBridge();
798
+ var defaultTokenBridge = createTokenBridge();
799
+
800
+ // src/components/TokenBridge.tsx
801
+ var TokenBridgeSync = ({
802
+ bridge = defaultTokenBridge
803
+ }) => {
804
+ const auth = useAuth();
805
+ useEffect(() => {
806
+ const token = auth.user?.access_token ?? null;
807
+ bridge.setToken(token);
808
+ }, [auth.user, bridge]);
809
+ useEffect(() => {
810
+ if (!auth.isAuthenticated) {
811
+ bridge.setToken(null);
812
+ }
813
+ }, [auth.isAuthenticated, bridge]);
814
+ return null;
815
+ };
816
+
659
817
  // src/config/presets.ts
660
818
  var CONFIG_PRESETS = {
661
819
  /**
@@ -874,11 +1032,97 @@ var StorageConfigProvider = ({ storageType, children }) => {
874
1032
  return /* @__PURE__ */ jsx(StorageConfigContext.Provider, { value: { storageType, storage }, children });
875
1033
  };
876
1034
  var useStorageConfig = () => useContext(StorageConfigContext);
1035
+ var defaultStatus = {
1036
+ isEnabled: true,
1037
+ isConfigured: true
1038
+ };
1039
+ var KeycloakStatusContext = createContext(defaultStatus);
1040
+ var KeycloakStatusProvider = KeycloakStatusContext.Provider;
1041
+ var useKeycloakStatus = () => useContext(KeycloakStatusContext);
1042
+ var createDisabledEvents = () => {
1043
+ const noop = () => void 0;
1044
+ const noopSubscription = () => () => void 0;
1045
+ const noopAsync = async () => void 0;
1046
+ const events = {
1047
+ addAccessTokenExpiring: noopSubscription,
1048
+ removeAccessTokenExpiring: noop,
1049
+ addAccessTokenExpired: noopSubscription,
1050
+ removeAccessTokenExpired: noop,
1051
+ addSilentRenewError: noopSubscription,
1052
+ removeSilentRenewError: noop,
1053
+ addUserLoaded: noopSubscription,
1054
+ removeUserLoaded: noop,
1055
+ addUserUnloaded: noopSubscription,
1056
+ removeUserUnloaded: noop,
1057
+ addUserSignedIn: noopSubscription,
1058
+ removeUserSignedIn: noop,
1059
+ addUserSignedOut: noopSubscription,
1060
+ removeUserSignedOut: noop,
1061
+ addUserSessionChanged: noopSubscription,
1062
+ removeUserSessionChanged: noop,
1063
+ load: noopAsync,
1064
+ unload: noopAsync
1065
+ };
1066
+ return events;
1067
+ };
1068
+ var disabledEvents = createDisabledEvents();
1069
+ var disabledSettings = {
1070
+ authority: "",
1071
+ client_id: "",
1072
+ redirect_uri: "http://localhost/disabled",
1073
+ response_type: "code",
1074
+ scope: ""
1075
+ };
1076
+ var disabledError = () => new Error("Keycloak integration is disabled for this environment.");
1077
+ var disabledAsync = async () => {
1078
+ throw disabledError();
1079
+ };
1080
+ var disabledAuthContext = {
1081
+ settings: disabledSettings,
1082
+ events: disabledEvents,
1083
+ clearStaleState: async () => void 0,
1084
+ removeUser: async () => void 0,
1085
+ signinPopup: (_args) => disabledAsync(),
1086
+ signinSilent: async (_args) => null,
1087
+ signinRedirect: (_args) => disabledAsync(),
1088
+ signinResourceOwnerCredentials: (_args) => disabledAsync(),
1089
+ signoutRedirect: async (_args) => void 0,
1090
+ signoutPopup: async (_args) => void 0,
1091
+ signoutSilent: async (_args) => void 0,
1092
+ querySessionStatus: async (_args) => null,
1093
+ revokeTokens: async (_types) => void 0,
1094
+ startSilentRenew: () => void 0,
1095
+ stopSilentRenew: () => void 0,
1096
+ user: null,
1097
+ isLoading: false,
1098
+ isAuthenticated: false,
1099
+ activeNavigator: void 0,
1100
+ error: void 0
1101
+ };
877
1102
  function isZeroConfig(props) {
878
1103
  return "authority" in props && "clientId" in props;
879
1104
  }
880
1105
  var KeycloakProvider = (props) => {
881
1106
  const { children, debug = false } = props;
1107
+ const status = useMemo(() => {
1108
+ const enabledFlag = props.enabled ?? true;
1109
+ const hasAuthority = isZeroConfig(props) ? Boolean(props.authority) : Boolean(props.config.authority);
1110
+ const hasClientId = isZeroConfig(props) ? Boolean(props.clientId) : Boolean(props.config.clientId);
1111
+ return {
1112
+ isEnabled: enabledFlag,
1113
+ isConfigured: hasAuthority && hasClientId,
1114
+ reason: !enabledFlag ? "disabled" : !hasAuthority || !hasClientId ? "missing-config" : void 0
1115
+ };
1116
+ }, [props]);
1117
+ const effectiveEnabled = status.isEnabled && status.isConfigured;
1118
+ useEffect(() => {
1119
+ if (!effectiveEnabled && props.tokenBridge) {
1120
+ props.tokenBridge.setToken(null);
1121
+ }
1122
+ }, [effectiveEnabled, props.tokenBridge]);
1123
+ if (!effectiveEnabled) {
1124
+ return /* @__PURE__ */ jsx(KeycloakStatusProvider, { value: status, children: /* @__PURE__ */ jsx(AuthContext.Provider, { value: disabledAuthContext, children: /* @__PURE__ */ jsx(StorageConfigProvider, { storageType: "session", children }) }) });
1125
+ }
882
1126
  const config = isZeroConfig(props) ? buildZeroConfig(props) : props.config;
883
1127
  const oidcConfig = KeycloakConfigBuilder.build(config);
884
1128
  const log = useCallback(
@@ -920,7 +1164,7 @@ var KeycloakProvider = (props) => {
920
1164
  }, [log]);
921
1165
  const showSessionMonitor = config.features?.sessionMonitor !== false;
922
1166
  const storageType = config.advanced?.storageType || "session";
923
- return /* @__PURE__ */ jsx(
1167
+ return /* @__PURE__ */ jsx(KeycloakStatusProvider, { value: status, children: /* @__PURE__ */ jsx(
924
1168
  AuthProvider,
925
1169
  {
926
1170
  ...oidcConfig,
@@ -928,11 +1172,12 @@ var KeycloakProvider = (props) => {
928
1172
  onSignoutCallback,
929
1173
  onRemoveUser,
930
1174
  children: /* @__PURE__ */ jsx(StorageConfigProvider, { storageType, children: /* @__PURE__ */ jsxs(TokenLifecycleManager, { config, debug, children: [
1175
+ props.tokenBridge && /* @__PURE__ */ jsx(TokenBridgeSync, { bridge: props.tokenBridge }),
931
1176
  showSessionMonitor && /* @__PURE__ */ jsx(SessionMonitor, {}),
932
1177
  children
933
1178
  ] }) })
934
1179
  }
935
- );
1180
+ ) });
936
1181
  };
937
1182
  function buildZeroConfig(props) {
938
1183
  const { authority, clientId, preset = "default", config: overrides = {} } = props;
@@ -1636,6 +1881,6 @@ function useProtectedFetch(options = {}) {
1636
1881
  };
1637
1882
  }
1638
1883
 
1639
- export { CONFIG_PRESETS, DeviceLogin, KeycloakConfigBuilder, KeycloakProvider, LoginButton, LogoutButton, ProtectedRoute, SessionMonitor, TokenLifecycleManager, UserProfile, defaultConfig, fetchUserInfo, getConfigFromEnv, getConfigPreset, mergeWithPreset, pollForToken, requestDeviceCode, useAuth, useProtectedFetch, useRoles, useScopes, useTokenLifecycle };
1884
+ export { CONFIG_PRESETS, DeviceLogin, KeycloakConfigBuilder, KeycloakProvider, KeycloakStatusProvider, LoginButton, LogoutButton, ProtectedRoute, SessionMonitor, TokenBridge, TokenBridgeSync, TokenLifecycleManager, UserProfile, createTokenBridge, defaultConfig, defaultTokenBridge, fetchUserInfo, getConfigFromEnv, getConfigPreset, mergeWithPreset, pollForToken, requestDeviceCode, useAuth, useKeycloakStatus, useProtectedFetch, useRoles, useScopes, useTokenLifecycle };
1640
1885
  //# sourceMappingURL=index.mjs.map
1641
1886
  //# sourceMappingURL=index.mjs.map