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.js CHANGED
@@ -152,6 +152,14 @@ var useAuth = reactOidcContext.useAuth;
152
152
 
153
153
  // src/hooks/useTokenLifecycle.ts
154
154
  var STORAGE_KEY_PREFIX = "keycloak_first_login_";
155
+ var isNavigatorOffline = () => typeof navigator !== "undefined" && "onLine" in navigator && navigator.onLine === false;
156
+ var isTransientNetworkError = (error) => {
157
+ const message = error.message?.toLowerCase?.() ?? "";
158
+ if (!message) {
159
+ return false;
160
+ }
161
+ 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");
162
+ };
155
163
  function useTokenLifecycle(options) {
156
164
  const {
157
165
  config,
@@ -166,6 +174,11 @@ function useTokenLifecycle(options) {
166
174
  const [isRefreshTokenExpired, setIsRefreshTokenExpired] = react.useState(false);
167
175
  const hasCheckedOnStartup = react.useRef(false);
168
176
  const supportsRefreshTokens = config.silentRenew?.useRefreshToken !== false;
177
+ const [pendingRetry, setPendingRetry] = react.useState(null);
178
+ const deferredRefreshReasonRef = react.useRef(null);
179
+ const enqueueRetry = react.useCallback((reason) => {
180
+ setPendingRetry((current) => current === reason ? current : reason);
181
+ }, []);
169
182
  const log = react.useCallback(
170
183
  (message, ...args) => {
171
184
  if (debug) {
@@ -216,6 +229,7 @@ function useTokenLifecycle(options) {
216
229
  return;
217
230
  }
218
231
  setIsRefreshing(true);
232
+ deferredRefreshReasonRef.current = null;
219
233
  try {
220
234
  if (checkRefreshTokenLifetime()) {
221
235
  log("Refresh token exceeded max lifetime, cannot refresh");
@@ -223,6 +237,12 @@ function useTokenLifecycle(options) {
223
237
  onRefreshTokenExpired?.();
224
238
  return;
225
239
  }
240
+ if (isNavigatorOffline()) {
241
+ log("Device offline, deferring token refresh until connection is restored");
242
+ deferredRefreshReasonRef.current = "offline";
243
+ enqueueRetry("offline");
244
+ return;
245
+ }
226
246
  onRefreshStart?.();
227
247
  log("Starting token refresh");
228
248
  await auth.signinSilent();
@@ -230,6 +250,21 @@ function useTokenLifecycle(options) {
230
250
  onRefreshSuccess?.();
231
251
  } catch (error) {
232
252
  const err = error instanceof Error ? error : new Error(String(error));
253
+ if (isTransientNetworkError(err)) {
254
+ const reason = isNavigatorOffline() ? "offline" : "network";
255
+ deferredRefreshReasonRef.current = reason;
256
+ if (reason === "offline") {
257
+ log("Token refresh deferred because device is offline");
258
+ } else {
259
+ log(
260
+ "Token refresh failed due to transient network issue, will retry automatically:",
261
+ err.message
262
+ );
263
+ }
264
+ enqueueRetry(reason);
265
+ onRefreshError?.(err);
266
+ return;
267
+ }
233
268
  log("Token refresh failed:", err.message);
234
269
  onRefreshError?.(err);
235
270
  throw err;
@@ -244,6 +279,7 @@ function useTokenLifecycle(options) {
244
279
  onRefreshSuccess,
245
280
  onRefreshError,
246
281
  onRefreshTokenExpired,
282
+ enqueueRetry,
247
283
  log
248
284
  ]);
249
285
  const checkAndRefreshTokens = react.useCallback(async () => {
@@ -282,6 +318,14 @@ function useTokenLifecycle(options) {
282
318
  if (timeUntilExpiry <= threshold) {
283
319
  try {
284
320
  await refreshToken();
321
+ if (deferredRefreshReasonRef.current) {
322
+ log(
323
+ "Token refresh was deferred due to pending retry:",
324
+ deferredRefreshReasonRef.current
325
+ );
326
+ deferredRefreshReasonRef.current = null;
327
+ return false;
328
+ }
285
329
  return true;
286
330
  } catch (error) {
287
331
  log("Failed to refresh tokens:", error);
@@ -299,6 +343,55 @@ function useTokenLifecycle(options) {
299
343
  getStorageKey,
300
344
  log
301
345
  ]);
346
+ react.useEffect(() => {
347
+ if (pendingRetry === null) {
348
+ return;
349
+ }
350
+ if (typeof window === "undefined") {
351
+ return;
352
+ }
353
+ let cancelled = false;
354
+ const executeRetry = () => {
355
+ if (cancelled) {
356
+ return;
357
+ }
358
+ if (isRefreshing) {
359
+ log("Skipping scheduled token refresh retry because a refresh is already in progress");
360
+ return;
361
+ }
362
+ refreshToken().catch((err) => {
363
+ log("Scheduled token refresh retry failed:", err);
364
+ });
365
+ };
366
+ if (pendingRetry === "offline") {
367
+ log("Waiting for online event to retry token refresh");
368
+ const handleOnline = () => {
369
+ if (cancelled) {
370
+ return;
371
+ }
372
+ log("Online event detected, retrying deferred token refresh");
373
+ setPendingRetry(null);
374
+ executeRetry();
375
+ };
376
+ window.addEventListener("online", handleOnline, { once: true });
377
+ return () => {
378
+ cancelled = true;
379
+ window.removeEventListener("online", handleOnline);
380
+ };
381
+ }
382
+ const timeoutId = window.setTimeout(() => {
383
+ if (cancelled) {
384
+ return;
385
+ }
386
+ log("Retrying token refresh after transient network failure delay");
387
+ setPendingRetry(null);
388
+ executeRetry();
389
+ }, 5e3);
390
+ return () => {
391
+ cancelled = true;
392
+ window.clearTimeout(timeoutId);
393
+ };
394
+ }, [pendingRetry, refreshToken, isRefreshing, log]);
302
395
  react.useEffect(() => {
303
396
  const strategy = config.silentRenew?.refreshStrategy || "both";
304
397
  const shouldCheckOnStartup = strategy === "startup" || strategy === "both";
@@ -659,6 +752,71 @@ var SessionMonitor = ({
659
752
  ] });
660
753
  };
661
754
 
755
+ // src/utils/tokenBridge.ts
756
+ var TokenBridge = class {
757
+ constructor() {
758
+ this.currentToken = null;
759
+ this.listeners = /* @__PURE__ */ new Set();
760
+ }
761
+ /**
762
+ * Update the tracked token and notify subscribers when it changes.
763
+ */
764
+ setToken(token) {
765
+ if (this.currentToken === token) {
766
+ return;
767
+ }
768
+ this.currentToken = token;
769
+ for (const listener of this.listeners) {
770
+ try {
771
+ listener(token);
772
+ } catch (error) {
773
+ console.error("[TokenBridge] Listener threw error:", error);
774
+ }
775
+ }
776
+ }
777
+ /**
778
+ * Read the most recent token value.
779
+ */
780
+ getToken() {
781
+ return this.currentToken;
782
+ }
783
+ /**
784
+ * Convenience helper for async callers.
785
+ */
786
+ async getTokenAsync() {
787
+ return this.currentToken;
788
+ }
789
+ /**
790
+ * Subscribe to token changes. Returns an unsubscribe callback.
791
+ */
792
+ subscribe(listener) {
793
+ this.listeners.add(listener);
794
+ listener(this.currentToken);
795
+ return () => {
796
+ this.listeners.delete(listener);
797
+ };
798
+ }
799
+ };
800
+ var createTokenBridge = () => new TokenBridge();
801
+ var defaultTokenBridge = createTokenBridge();
802
+
803
+ // src/components/TokenBridge.tsx
804
+ var TokenBridgeSync = ({
805
+ bridge = defaultTokenBridge
806
+ }) => {
807
+ const auth = useAuth();
808
+ react.useEffect(() => {
809
+ const token = auth.user?.access_token ?? null;
810
+ bridge.setToken(token);
811
+ }, [auth.user, bridge]);
812
+ react.useEffect(() => {
813
+ if (!auth.isAuthenticated) {
814
+ bridge.setToken(null);
815
+ }
816
+ }, [auth.isAuthenticated, bridge]);
817
+ return null;
818
+ };
819
+
662
820
  // src/config/presets.ts
663
821
  var CONFIG_PRESETS = {
664
822
  /**
@@ -877,11 +1035,97 @@ var StorageConfigProvider = ({ storageType, children }) => {
877
1035
  return /* @__PURE__ */ jsxRuntime.jsx(StorageConfigContext.Provider, { value: { storageType, storage }, children });
878
1036
  };
879
1037
  var useStorageConfig = () => react.useContext(StorageConfigContext);
1038
+ var defaultStatus = {
1039
+ isEnabled: true,
1040
+ isConfigured: true
1041
+ };
1042
+ var KeycloakStatusContext = react.createContext(defaultStatus);
1043
+ var KeycloakStatusProvider = KeycloakStatusContext.Provider;
1044
+ var useKeycloakStatus = () => react.useContext(KeycloakStatusContext);
1045
+ var createDisabledEvents = () => {
1046
+ const noop = () => void 0;
1047
+ const noopSubscription = () => () => void 0;
1048
+ const noopAsync = async () => void 0;
1049
+ const events = {
1050
+ addAccessTokenExpiring: noopSubscription,
1051
+ removeAccessTokenExpiring: noop,
1052
+ addAccessTokenExpired: noopSubscription,
1053
+ removeAccessTokenExpired: noop,
1054
+ addSilentRenewError: noopSubscription,
1055
+ removeSilentRenewError: noop,
1056
+ addUserLoaded: noopSubscription,
1057
+ removeUserLoaded: noop,
1058
+ addUserUnloaded: noopSubscription,
1059
+ removeUserUnloaded: noop,
1060
+ addUserSignedIn: noopSubscription,
1061
+ removeUserSignedIn: noop,
1062
+ addUserSignedOut: noopSubscription,
1063
+ removeUserSignedOut: noop,
1064
+ addUserSessionChanged: noopSubscription,
1065
+ removeUserSessionChanged: noop,
1066
+ load: noopAsync,
1067
+ unload: noopAsync
1068
+ };
1069
+ return events;
1070
+ };
1071
+ var disabledEvents = createDisabledEvents();
1072
+ var disabledSettings = {
1073
+ authority: "",
1074
+ client_id: "",
1075
+ redirect_uri: "http://localhost/disabled",
1076
+ response_type: "code",
1077
+ scope: ""
1078
+ };
1079
+ var disabledError = () => new Error("Keycloak integration is disabled for this environment.");
1080
+ var disabledAsync = async () => {
1081
+ throw disabledError();
1082
+ };
1083
+ var disabledAuthContext = {
1084
+ settings: disabledSettings,
1085
+ events: disabledEvents,
1086
+ clearStaleState: async () => void 0,
1087
+ removeUser: async () => void 0,
1088
+ signinPopup: (_args) => disabledAsync(),
1089
+ signinSilent: async (_args) => null,
1090
+ signinRedirect: (_args) => disabledAsync(),
1091
+ signinResourceOwnerCredentials: (_args) => disabledAsync(),
1092
+ signoutRedirect: async (_args) => void 0,
1093
+ signoutPopup: async (_args) => void 0,
1094
+ signoutSilent: async (_args) => void 0,
1095
+ querySessionStatus: async (_args) => null,
1096
+ revokeTokens: async (_types) => void 0,
1097
+ startSilentRenew: () => void 0,
1098
+ stopSilentRenew: () => void 0,
1099
+ user: null,
1100
+ isLoading: false,
1101
+ isAuthenticated: false,
1102
+ activeNavigator: void 0,
1103
+ error: void 0
1104
+ };
880
1105
  function isZeroConfig(props) {
881
1106
  return "authority" in props && "clientId" in props;
882
1107
  }
883
1108
  var KeycloakProvider = (props) => {
884
1109
  const { children, debug = false } = props;
1110
+ const status = react.useMemo(() => {
1111
+ const enabledFlag = props.enabled ?? true;
1112
+ const hasAuthority = isZeroConfig(props) ? Boolean(props.authority) : Boolean(props.config.authority);
1113
+ const hasClientId = isZeroConfig(props) ? Boolean(props.clientId) : Boolean(props.config.clientId);
1114
+ return {
1115
+ isEnabled: enabledFlag,
1116
+ isConfigured: hasAuthority && hasClientId,
1117
+ reason: !enabledFlag ? "disabled" : !hasAuthority || !hasClientId ? "missing-config" : void 0
1118
+ };
1119
+ }, [props]);
1120
+ const effectiveEnabled = status.isEnabled && status.isConfigured;
1121
+ react.useEffect(() => {
1122
+ if (!effectiveEnabled && props.tokenBridge) {
1123
+ props.tokenBridge.setToken(null);
1124
+ }
1125
+ }, [effectiveEnabled, props.tokenBridge]);
1126
+ if (!effectiveEnabled) {
1127
+ return /* @__PURE__ */ jsxRuntime.jsx(KeycloakStatusProvider, { value: status, children: /* @__PURE__ */ jsxRuntime.jsx(reactOidcContext.AuthContext.Provider, { value: disabledAuthContext, children: /* @__PURE__ */ jsxRuntime.jsx(StorageConfigProvider, { storageType: "session", children }) }) });
1128
+ }
885
1129
  const config = isZeroConfig(props) ? buildZeroConfig(props) : props.config;
886
1130
  const oidcConfig = KeycloakConfigBuilder.build(config);
887
1131
  const log = react.useCallback(
@@ -923,7 +1167,7 @@ var KeycloakProvider = (props) => {
923
1167
  }, [log]);
924
1168
  const showSessionMonitor = config.features?.sessionMonitor !== false;
925
1169
  const storageType = config.advanced?.storageType || "session";
926
- return /* @__PURE__ */ jsxRuntime.jsx(
1170
+ return /* @__PURE__ */ jsxRuntime.jsx(KeycloakStatusProvider, { value: status, children: /* @__PURE__ */ jsxRuntime.jsx(
927
1171
  reactOidcContext.AuthProvider,
928
1172
  {
929
1173
  ...oidcConfig,
@@ -931,11 +1175,12 @@ var KeycloakProvider = (props) => {
931
1175
  onSignoutCallback,
932
1176
  onRemoveUser,
933
1177
  children: /* @__PURE__ */ jsxRuntime.jsx(StorageConfigProvider, { storageType, children: /* @__PURE__ */ jsxRuntime.jsxs(TokenLifecycleManager, { config, debug, children: [
1178
+ props.tokenBridge && /* @__PURE__ */ jsxRuntime.jsx(TokenBridgeSync, { bridge: props.tokenBridge }),
934
1179
  showSessionMonitor && /* @__PURE__ */ jsxRuntime.jsx(SessionMonitor, {}),
935
1180
  children
936
1181
  ] }) })
937
1182
  }
938
- );
1183
+ ) });
939
1184
  };
940
1185
  function buildZeroConfig(props) {
941
1186
  const { authority, clientId, preset = "default", config: overrides = {} } = props;
@@ -1643,13 +1888,18 @@ exports.CONFIG_PRESETS = CONFIG_PRESETS;
1643
1888
  exports.DeviceLogin = DeviceLogin;
1644
1889
  exports.KeycloakConfigBuilder = KeycloakConfigBuilder;
1645
1890
  exports.KeycloakProvider = KeycloakProvider;
1891
+ exports.KeycloakStatusProvider = KeycloakStatusProvider;
1646
1892
  exports.LoginButton = LoginButton;
1647
1893
  exports.LogoutButton = LogoutButton;
1648
1894
  exports.ProtectedRoute = ProtectedRoute;
1649
1895
  exports.SessionMonitor = SessionMonitor;
1896
+ exports.TokenBridge = TokenBridge;
1897
+ exports.TokenBridgeSync = TokenBridgeSync;
1650
1898
  exports.TokenLifecycleManager = TokenLifecycleManager;
1651
1899
  exports.UserProfile = UserProfile;
1900
+ exports.createTokenBridge = createTokenBridge;
1652
1901
  exports.defaultConfig = defaultConfig;
1902
+ exports.defaultTokenBridge = defaultTokenBridge;
1653
1903
  exports.fetchUserInfo = fetchUserInfo;
1654
1904
  exports.getConfigFromEnv = getConfigFromEnv;
1655
1905
  exports.getConfigPreset = getConfigPreset;
@@ -1657,6 +1907,7 @@ exports.mergeWithPreset = mergeWithPreset;
1657
1907
  exports.pollForToken = pollForToken;
1658
1908
  exports.requestDeviceCode = requestDeviceCode;
1659
1909
  exports.useAuth = useAuth;
1910
+ exports.useKeycloakStatus = useKeycloakStatus;
1660
1911
  exports.useProtectedFetch = useProtectedFetch;
1661
1912
  exports.useRoles = useRoles;
1662
1913
  exports.useScopes = useScopes;