antaeus.keycloak.react 1.0.6 → 2.0.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
@@ -23,8 +23,14 @@ var defaultConfig = {
23
23
  silentRenew: {
24
24
  enabled: true,
25
25
  silentRedirectUri: typeof window !== "undefined" ? window.location.origin + "/silent-renew.html" : "/silent-renew.html",
26
- thresholdSeconds: 300
26
+ thresholdSeconds: 300,
27
27
  // 5 minutes
28
+ refreshStrategy: "both",
29
+ // Check on startup AND retry on 401
30
+ maxRefreshTokenLifetime: 2592e3,
31
+ // 30 days in seconds
32
+ showLoadingIndicator: false,
33
+ loadingMessage: "Refreshing session..."
28
34
  },
29
35
  advanced: {
30
36
  responseType: "code",
@@ -128,16 +134,693 @@ function getConfigFromEnv() {
128
134
  }
129
135
  };
130
136
  }
131
- var KeycloakProvider = ({
137
+ var useAuth = reactOidcContext.useAuth;
138
+
139
+ // src/hooks/useTokenLifecycle.ts
140
+ var STORAGE_KEY_PREFIX = "keycloak_first_login_";
141
+ function useTokenLifecycle(options) {
142
+ const {
143
+ config,
144
+ debug = false,
145
+ onRefreshStart,
146
+ onRefreshSuccess,
147
+ onRefreshError,
148
+ onRefreshTokenExpired
149
+ } = options;
150
+ const auth = useAuth();
151
+ const [isRefreshing, setIsRefreshing] = react.useState(false);
152
+ const [isRefreshTokenExpired, setIsRefreshTokenExpired] = react.useState(false);
153
+ const hasCheckedOnStartup = react.useRef(false);
154
+ const log = react.useCallback(
155
+ (message, ...args) => {
156
+ if (debug) {
157
+ console.log("[TokenLifecycle]", message, ...args);
158
+ }
159
+ },
160
+ [debug]
161
+ );
162
+ const getStorageKey = react.useCallback(() => {
163
+ const authority = config.authority;
164
+ const clientId = config.clientId;
165
+ return `${STORAGE_KEY_PREFIX}${authority}_${clientId}`;
166
+ }, [config.authority, config.clientId]);
167
+ const storeFirstLoginTime = react.useCallback(() => {
168
+ const key = getStorageKey();
169
+ const existingTime = localStorage.getItem(key);
170
+ if (!existingTime) {
171
+ const now = Date.now();
172
+ localStorage.setItem(key, now.toString());
173
+ log("Stored first login time:", new Date(now).toISOString());
174
+ }
175
+ }, [getStorageKey, log]);
176
+ const checkRefreshTokenLifetime = react.useCallback(() => {
177
+ const maxLifetime = config.silentRenew?.maxRefreshTokenLifetime;
178
+ if (!maxLifetime || maxLifetime <= 0) {
179
+ return false;
180
+ }
181
+ const key = getStorageKey();
182
+ const firstLoginStr = localStorage.getItem(key);
183
+ if (!firstLoginStr) {
184
+ storeFirstLoginTime();
185
+ return false;
186
+ }
187
+ const firstLoginTime = parseInt(firstLoginStr, 10);
188
+ const now = Date.now();
189
+ const elapsedSeconds = (now - firstLoginTime) / 1e3;
190
+ log("Refresh token lifetime check:", {
191
+ firstLogin: new Date(firstLoginTime).toISOString(),
192
+ elapsedSeconds,
193
+ maxLifetime,
194
+ isExpired: elapsedSeconds >= maxLifetime
195
+ });
196
+ return elapsedSeconds >= maxLifetime;
197
+ }, [config.silentRenew?.maxRefreshTokenLifetime, getStorageKey, log, storeFirstLoginTime]);
198
+ const refreshToken = react.useCallback(async () => {
199
+ if (isRefreshing) {
200
+ log("Refresh already in progress, skipping");
201
+ return;
202
+ }
203
+ if (checkRefreshTokenLifetime()) {
204
+ log("Refresh token exceeded max lifetime, cannot refresh");
205
+ setIsRefreshTokenExpired(true);
206
+ onRefreshTokenExpired?.();
207
+ return;
208
+ }
209
+ try {
210
+ setIsRefreshing(true);
211
+ onRefreshStart?.();
212
+ log("Starting token refresh");
213
+ await auth.signinSilent();
214
+ log("Token refresh successful");
215
+ onRefreshSuccess?.();
216
+ } catch (error) {
217
+ const err = error instanceof Error ? error : new Error(String(error));
218
+ log("Token refresh failed:", err.message);
219
+ onRefreshError?.(err);
220
+ throw err;
221
+ } finally {
222
+ setIsRefreshing(false);
223
+ }
224
+ }, [
225
+ isRefreshing,
226
+ auth,
227
+ checkRefreshTokenLifetime,
228
+ onRefreshStart,
229
+ onRefreshSuccess,
230
+ onRefreshError,
231
+ onRefreshTokenExpired,
232
+ log
233
+ ]);
234
+ const checkAndRefreshTokens = react.useCallback(async () => {
235
+ if (!auth.user) {
236
+ log("No user session, skipping token check");
237
+ return false;
238
+ }
239
+ if (checkRefreshTokenLifetime()) {
240
+ log("Refresh token exceeded max lifetime");
241
+ setIsRefreshTokenExpired(true);
242
+ onRefreshTokenExpired?.();
243
+ const key = getStorageKey();
244
+ localStorage.removeItem(key);
245
+ await auth.removeUser();
246
+ return false;
247
+ }
248
+ const expiresAt = auth.user.expires_at;
249
+ if (!expiresAt) {
250
+ log("No expiration time found, skipping token check");
251
+ return false;
252
+ }
253
+ const now = Math.floor(Date.now() / 1e3);
254
+ const threshold = config.silentRenew?.thresholdSeconds || 300;
255
+ const timeUntilExpiry = expiresAt - now;
256
+ log("Token expiration check:", {
257
+ expiresAt: new Date(expiresAt * 1e3).toISOString(),
258
+ now: new Date(now * 1e3).toISOString(),
259
+ timeUntilExpiry,
260
+ threshold,
261
+ needsRefresh: timeUntilExpiry <= threshold
262
+ });
263
+ if (timeUntilExpiry <= threshold) {
264
+ try {
265
+ await refreshToken();
266
+ return true;
267
+ } catch (error) {
268
+ log("Failed to refresh tokens:", error);
269
+ return false;
270
+ }
271
+ }
272
+ log("Tokens are still valid, no refresh needed");
273
+ return false;
274
+ }, [
275
+ auth,
276
+ config.silentRenew?.thresholdSeconds,
277
+ checkRefreshTokenLifetime,
278
+ refreshToken,
279
+ onRefreshTokenExpired,
280
+ getStorageKey,
281
+ log
282
+ ]);
283
+ react.useEffect(() => {
284
+ const strategy = config.silentRenew?.refreshStrategy || "both";
285
+ const shouldCheckOnStartup = strategy === "startup" || strategy === "both";
286
+ if (!shouldCheckOnStartup || hasCheckedOnStartup.current) {
287
+ return;
288
+ }
289
+ if (!auth.isAuthenticated || auth.isLoading) {
290
+ return;
291
+ }
292
+ hasCheckedOnStartup.current = true;
293
+ log("Running startup token check with strategy:", strategy);
294
+ checkAndRefreshTokens().catch((error) => {
295
+ log("Startup token check failed:", error);
296
+ });
297
+ }, [
298
+ auth.isAuthenticated,
299
+ auth.isLoading,
300
+ config.silentRenew?.refreshStrategy,
301
+ checkAndRefreshTokens,
302
+ log
303
+ ]);
304
+ react.useEffect(() => {
305
+ if (auth.user && !auth.isLoading) {
306
+ storeFirstLoginTime();
307
+ }
308
+ }, [auth.user, auth.isLoading, storeFirstLoginTime]);
309
+ react.useEffect(() => {
310
+ if (!auth.isAuthenticated || auth.isLoading) {
311
+ return;
312
+ }
313
+ const maxLifetime = config.silentRenew?.maxRefreshTokenLifetime;
314
+ if (!maxLifetime || maxLifetime <= 0) {
315
+ return;
316
+ }
317
+ const intervalId = setInterval(() => {
318
+ if (checkRefreshTokenLifetime()) {
319
+ log("Refresh token expired during periodic check");
320
+ setIsRefreshTokenExpired(true);
321
+ onRefreshTokenExpired?.();
322
+ const key = getStorageKey();
323
+ localStorage.removeItem(key);
324
+ auth.removeUser();
325
+ }
326
+ }, 5 * 60 * 1e3);
327
+ return () => clearInterval(intervalId);
328
+ }, [
329
+ auth,
330
+ config.silentRenew?.maxRefreshTokenLifetime,
331
+ checkRefreshTokenLifetime,
332
+ onRefreshTokenExpired,
333
+ getStorageKey,
334
+ log
335
+ ]);
336
+ return {
337
+ isRefreshing,
338
+ isRefreshTokenExpired,
339
+ refreshToken,
340
+ checkAndRefreshTokens
341
+ };
342
+ }
343
+ var TokenLifecycleManager = ({
132
344
  config,
133
345
  children,
134
346
  debug = false
135
347
  }) => {
348
+ const [showLoading, setShowLoading] = react.useState(false);
349
+ const handleRefreshStart = react.useCallback(() => {
350
+ if (config.silentRenew?.showLoadingIndicator) {
351
+ setShowLoading(true);
352
+ }
353
+ }, [config.silentRenew?.showLoadingIndicator]);
354
+ const handleRefreshSuccess = react.useCallback(() => {
355
+ setShowLoading(false);
356
+ }, []);
357
+ const handleRefreshError = react.useCallback((error) => {
358
+ setShowLoading(false);
359
+ if (debug) {
360
+ console.error("[TokenLifecycleManager] Token refresh failed:", error);
361
+ }
362
+ config.events?.onSilentRenewError?.(error);
363
+ }, [config.events, debug]);
364
+ const handleRefreshTokenExpired = react.useCallback(() => {
365
+ setShowLoading(false);
366
+ if (debug) {
367
+ console.warn("[TokenLifecycleManager] Refresh token exceeded max lifetime");
368
+ }
369
+ config.events?.onTokenExpired?.();
370
+ }, [config.events, debug]);
371
+ const { isRefreshing, isRefreshTokenExpired } = useTokenLifecycle({
372
+ config,
373
+ debug,
374
+ onRefreshStart: handleRefreshStart,
375
+ onRefreshSuccess: handleRefreshSuccess,
376
+ onRefreshError: handleRefreshError,
377
+ onRefreshTokenExpired: handleRefreshTokenExpired
378
+ });
379
+ if (isRefreshTokenExpired) {
380
+ return /* @__PURE__ */ jsxRuntime.jsx(
381
+ "div",
382
+ {
383
+ style: {
384
+ display: "flex",
385
+ flexDirection: "column",
386
+ alignItems: "center",
387
+ justifyContent: "center",
388
+ height: "100vh",
389
+ fontFamily: "system-ui, -apple-system, sans-serif",
390
+ backgroundColor: "#f5f5f5",
391
+ padding: "20px",
392
+ textAlign: "center"
393
+ },
394
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
395
+ "div",
396
+ {
397
+ style: {
398
+ backgroundColor: "white",
399
+ padding: "40px",
400
+ borderRadius: "8px",
401
+ boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
402
+ maxWidth: "400px"
403
+ },
404
+ children: [
405
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { style: { margin: "0 0 16px 0", color: "#333" }, children: "Session Expired" }),
406
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: "0 0 24px 0", color: "#666" }, children: "Your session has been inactive for too long. Please sign in again to continue." }),
407
+ /* @__PURE__ */ jsxRuntime.jsx(
408
+ "button",
409
+ {
410
+ onClick: () => window.location.reload(),
411
+ style: {
412
+ padding: "12px 24px",
413
+ backgroundColor: "#007bff",
414
+ color: "white",
415
+ border: "none",
416
+ borderRadius: "4px",
417
+ fontSize: "16px",
418
+ cursor: "pointer",
419
+ fontWeight: "500"
420
+ },
421
+ children: "Sign In Again"
422
+ }
423
+ )
424
+ ]
425
+ }
426
+ )
427
+ }
428
+ );
429
+ }
430
+ if (showLoading && isRefreshing) {
431
+ const message = config.silentRenew?.loadingMessage || "Refreshing session...";
432
+ return /* @__PURE__ */ jsxRuntime.jsxs(
433
+ "div",
434
+ {
435
+ style: {
436
+ display: "flex",
437
+ flexDirection: "column",
438
+ alignItems: "center",
439
+ justifyContent: "center",
440
+ height: "100vh",
441
+ fontFamily: "system-ui, -apple-system, sans-serif",
442
+ backgroundColor: "#f5f5f5"
443
+ },
444
+ children: [
445
+ /* @__PURE__ */ jsxRuntime.jsxs(
446
+ "div",
447
+ {
448
+ style: {
449
+ display: "flex",
450
+ flexDirection: "column",
451
+ alignItems: "center",
452
+ gap: "16px"
453
+ },
454
+ children: [
455
+ /* @__PURE__ */ jsxRuntime.jsx(
456
+ "div",
457
+ {
458
+ style: {
459
+ width: "40px",
460
+ height: "40px",
461
+ border: "4px solid #e0e0e0",
462
+ borderTop: "4px solid #007bff",
463
+ borderRadius: "50%",
464
+ animation: "spin 1s linear infinite"
465
+ }
466
+ }
467
+ ),
468
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { margin: 0, color: "#666", fontSize: "16px" }, children: message })
469
+ ]
470
+ }
471
+ ),
472
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
473
+ @keyframes spin {
474
+ 0% { transform: rotate(0deg); }
475
+ 100% { transform: rotate(360deg); }
476
+ }
477
+ ` })
478
+ ]
479
+ }
480
+ );
481
+ }
482
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
483
+ };
484
+ var SessionMonitor = ({
485
+ position = "top-right",
486
+ warningThreshold = 300,
487
+ // 5 minutes
488
+ criticalThreshold = 60,
489
+ // 1 minute
490
+ showProgressBar = true,
491
+ minimizable = true,
492
+ startMinimized = false,
493
+ className = "",
494
+ onWarning,
495
+ onCritical,
496
+ onExpired,
497
+ style
498
+ }) => {
499
+ const auth = useAuth();
500
+ const [timeRemaining, setTimeRemaining] = react.useState(null);
501
+ const [minimized, setMinimized] = react.useState(startMinimized);
502
+ const [warningTriggered, setWarningTriggered] = react.useState(false);
503
+ const [criticalTriggered, setCriticalTriggered] = react.useState(false);
504
+ react.useEffect(() => {
505
+ if (!auth.isAuthenticated || !auth.user?.expires_at) {
506
+ setTimeRemaining(null);
507
+ return;
508
+ }
509
+ const updateTimeRemaining = () => {
510
+ const expiresAt = auth.user.expires_at;
511
+ const now = Math.floor(Date.now() / 1e3);
512
+ const remaining = expiresAt - now;
513
+ if (remaining <= 0) {
514
+ setTimeRemaining(0);
515
+ onExpired?.();
516
+ setTimeout(() => {
517
+ auth.signoutRedirect();
518
+ }, 2e3);
519
+ } else {
520
+ setTimeRemaining(remaining);
521
+ if (remaining <= criticalThreshold && !criticalTriggered) {
522
+ setCriticalTriggered(true);
523
+ onCritical?.(remaining);
524
+ } else if (remaining <= warningThreshold && !warningTriggered) {
525
+ setWarningTriggered(true);
526
+ onWarning?.(remaining);
527
+ }
528
+ }
529
+ };
530
+ updateTimeRemaining();
531
+ const interval = setInterval(updateTimeRemaining, 1e3);
532
+ return () => clearInterval(interval);
533
+ }, [
534
+ auth,
535
+ warningThreshold,
536
+ criticalThreshold,
537
+ warningTriggered,
538
+ criticalTriggered,
539
+ onWarning,
540
+ onCritical,
541
+ onExpired
542
+ ]);
543
+ react.useEffect(() => {
544
+ if (timeRemaining && timeRemaining > warningThreshold) {
545
+ setWarningTriggered(false);
546
+ setCriticalTriggered(false);
547
+ }
548
+ }, [timeRemaining, warningThreshold]);
549
+ const formatTime = react.useCallback((seconds) => {
550
+ const mins = Math.floor(seconds / 60);
551
+ const secs = seconds % 60;
552
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
553
+ }, []);
554
+ const getStatusClass = react.useCallback(() => {
555
+ if (!timeRemaining) return "normal";
556
+ if (timeRemaining <= criticalThreshold) return "critical";
557
+ if (timeRemaining <= warningThreshold) return "warning";
558
+ return "normal";
559
+ }, [timeRemaining, warningThreshold, criticalThreshold]);
560
+ const getProgress = react.useCallback(() => {
561
+ if (!timeRemaining || !auth.user?.expires_in) return 100;
562
+ return timeRemaining / auth.user.expires_in * 100;
563
+ }, [timeRemaining, auth.user]);
564
+ if (!auth.isAuthenticated || timeRemaining === null) {
565
+ return null;
566
+ }
567
+ const baseClass = "kc-session-monitor";
568
+ const positionClass = `kc-session-monitor--${position}`;
569
+ const statusClass = `kc-session-monitor--${getStatusClass()}`;
570
+ const minimizedClass = minimized ? "kc-session-monitor--minimized" : "";
571
+ const classes = `${baseClass} ${positionClass} ${statusClass} ${minimizedClass} ${className}`.trim();
572
+ const progress = getProgress();
573
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classes, style, children: [
574
+ minimizable && /* @__PURE__ */ jsxRuntime.jsx(
575
+ "button",
576
+ {
577
+ className: "kc-session-monitor__toggle",
578
+ onClick: () => setMinimized(!minimized),
579
+ "aria-label": minimized ? "Expand session monitor" : "Minimize session monitor",
580
+ title: minimized ? "Expand" : "Minimize",
581
+ children: minimized ? "\u25B2" : "\u25BC"
582
+ }
583
+ ),
584
+ !minimized && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
585
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kc-session-monitor__content", children: [
586
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-session-monitor__label", children: timeRemaining === 0 ? "Session expired" : "Session expires in:" }),
587
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-session-monitor__time", children: formatTime(timeRemaining) })
588
+ ] }),
589
+ showProgressBar && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kc-session-monitor__progress-bar", children: /* @__PURE__ */ jsxRuntime.jsx(
590
+ "div",
591
+ {
592
+ className: "kc-session-monitor__progress-fill",
593
+ style: { width: `${progress}%` },
594
+ role: "progressbar",
595
+ "aria-valuenow": progress,
596
+ "aria-valuemin": 0,
597
+ "aria-valuemax": 100
598
+ }
599
+ ) })
600
+ ] })
601
+ ] });
602
+ };
603
+
604
+ // src/config/presets.ts
605
+ var CONFIG_PRESETS = {
606
+ /**
607
+ * Default preset - Balanced configuration for most applications
608
+ *
609
+ * Features:
610
+ * - 30-day refresh token lifetime
611
+ * - Proactive token refresh on startup + reactive on 401
612
+ * - Silent background token refresh
613
+ * - Session monitoring enabled
614
+ * - Auto token refresh enabled
615
+ *
616
+ * Best for: Most web applications, dashboards, internal tools
617
+ */
618
+ default: {
619
+ scope: "openid profile email offline_access",
620
+ features: {
621
+ autoRefresh: true,
622
+ sessionMonitor: true,
623
+ deviceFlow: false
624
+ },
625
+ silentRenew: {
626
+ enabled: true,
627
+ thresholdSeconds: 300,
628
+ // 5 minutes
629
+ refreshStrategy: "both",
630
+ maxRefreshTokenLifetime: 2592e3,
631
+ // 30 days
632
+ showLoadingIndicator: false,
633
+ loadingMessage: "Refreshing session..."
634
+ },
635
+ advanced: {
636
+ responseType: "code",
637
+ codeChallengeMethod: "S256",
638
+ loadUserInfo: true,
639
+ automaticSilentSignIn: false
640
+ }
641
+ },
642
+ /**
643
+ * Mobile/PWA preset - Optimized for mobile and progressive web apps
644
+ *
645
+ * Features:
646
+ * - 30-day refresh token lifetime
647
+ * - Proactive startup refresh (apps close frequently)
648
+ * - Loading indicator during refresh (better UX for app reopening)
649
+ * - Session monitoring enabled
650
+ *
651
+ * Best for: Mobile apps, PWAs, apps that frequently close/reopen
652
+ */
653
+ mobile: {
654
+ scope: "openid profile email offline_access",
655
+ features: {
656
+ autoRefresh: true,
657
+ sessionMonitor: true,
658
+ deviceFlow: false
659
+ },
660
+ silentRenew: {
661
+ enabled: true,
662
+ thresholdSeconds: 300,
663
+ refreshStrategy: "startup",
664
+ // Focus on startup refresh
665
+ maxRefreshTokenLifetime: 2592e3,
666
+ // 30 days
667
+ showLoadingIndicator: true,
668
+ // Show loading for better UX
669
+ loadingMessage: "Restoring your session..."
670
+ },
671
+ advanced: {
672
+ responseType: "code",
673
+ codeChallengeMethod: "S256",
674
+ loadUserInfo: true,
675
+ automaticSilentSignIn: false
676
+ }
677
+ },
678
+ /**
679
+ * High Security preset - Strict security for sensitive applications
680
+ *
681
+ * Features:
682
+ * - 24-hour forced re-authentication
683
+ * - Proactive + reactive token refresh
684
+ * - 10-minute refresh threshold (earlier refresh)
685
+ * - Silent refresh (no UI hints about security measures)
686
+ * - Session monitoring enabled
687
+ *
688
+ * Best for: Banking, healthcare, admin panels, sensitive data applications
689
+ */
690
+ "high-security": {
691
+ scope: "openid profile email offline_access",
692
+ features: {
693
+ autoRefresh: true,
694
+ sessionMonitor: true,
695
+ deviceFlow: false
696
+ },
697
+ silentRenew: {
698
+ enabled: true,
699
+ thresholdSeconds: 600,
700
+ // 10 minutes (more aggressive)
701
+ refreshStrategy: "both",
702
+ maxRefreshTokenLifetime: 86400,
703
+ // 24 hours only
704
+ showLoadingIndicator: false,
705
+ // Silent for security
706
+ loadingMessage: "Validating session..."
707
+ },
708
+ advanced: {
709
+ responseType: "code",
710
+ codeChallengeMethod: "S256",
711
+ loadUserInfo: true,
712
+ automaticSilentSignIn: false
713
+ }
714
+ },
715
+ /**
716
+ * Long-Running preset - Optimized for desktop apps and long sessions
717
+ *
718
+ * Features:
719
+ * - 7-day refresh token lifetime
720
+ * - Proactive + reactive token refresh
721
+ * - Silent background refresh
722
+ * - Session monitoring enabled
723
+ *
724
+ * Best for: Desktop applications, monitoring dashboards, always-on tools
725
+ */
726
+ "long-running": {
727
+ scope: "openid profile email offline_access",
728
+ features: {
729
+ autoRefresh: true,
730
+ sessionMonitor: true,
731
+ deviceFlow: false
732
+ },
733
+ silentRenew: {
734
+ enabled: true,
735
+ thresholdSeconds: 300,
736
+ refreshStrategy: "both",
737
+ maxRefreshTokenLifetime: 604800,
738
+ // 7 days
739
+ showLoadingIndicator: false,
740
+ loadingMessage: "Refreshing session..."
741
+ },
742
+ advanced: {
743
+ responseType: "code",
744
+ codeChallengeMethod: "S256",
745
+ loadUserInfo: true,
746
+ automaticSilentSignIn: false
747
+ }
748
+ },
749
+ /**
750
+ * Development preset - Convenient settings for development
751
+ *
752
+ * Features:
753
+ * - No refresh token lifetime limit (never expires)
754
+ * - Proactive + reactive token refresh
755
+ * - Loading indicators visible (easier debugging)
756
+ * - Session monitoring enabled
757
+ * - More verbose feedback
758
+ *
759
+ * Best for: Local development, testing, debugging
760
+ * WARNING: Do not use in production!
761
+ */
762
+ development: {
763
+ scope: "openid profile email offline_access",
764
+ features: {
765
+ autoRefresh: true,
766
+ sessionMonitor: true,
767
+ deviceFlow: false
768
+ },
769
+ silentRenew: {
770
+ enabled: true,
771
+ thresholdSeconds: 300,
772
+ refreshStrategy: "both",
773
+ maxRefreshTokenLifetime: 0,
774
+ // No limit (for convenience)
775
+ showLoadingIndicator: true,
776
+ // Show for debugging
777
+ loadingMessage: "[DEV] Refreshing tokens..."
778
+ },
779
+ advanced: {
780
+ responseType: "code",
781
+ codeChallengeMethod: "S256",
782
+ loadUserInfo: true,
783
+ automaticSilentSignIn: false
784
+ }
785
+ }
786
+ };
787
+ function getConfigPreset(preset) {
788
+ return CONFIG_PRESETS[preset];
789
+ }
790
+ function mergeWithPreset(preset, customConfig) {
791
+ const presetConfig = getConfigPreset(preset);
792
+ return {
793
+ ...presetConfig,
794
+ ...customConfig,
795
+ features: {
796
+ ...presetConfig.features,
797
+ ...customConfig.features
798
+ },
799
+ silentRenew: {
800
+ ...presetConfig.silentRenew,
801
+ ...customConfig.silentRenew
802
+ },
803
+ advanced: {
804
+ ...presetConfig.advanced,
805
+ ...customConfig.advanced
806
+ },
807
+ events: {
808
+ ...presetConfig.events,
809
+ ...customConfig.events
810
+ }
811
+ };
812
+ }
813
+ function isZeroConfig(props) {
814
+ return "authority" in props && "clientId" in props;
815
+ }
816
+ var KeycloakProvider = (props) => {
817
+ const { children, debug = false } = props;
818
+ const config = isZeroConfig(props) ? buildZeroConfig(props) : props.config;
136
819
  const oidcConfig = KeycloakConfigBuilder.build(config);
137
820
  const log = react.useCallback(
138
821
  (level, message, ...args) => {
139
822
  if (debug) {
140
- const prefix = "[Keycloak]";
823
+ const prefix = "[KeycloakProvider]";
141
824
  switch (level) {
142
825
  case "info":
143
826
  console.log(prefix, message, ...args);
@@ -165,11 +848,13 @@ var KeycloakProvider = ({
165
848
  );
166
849
  const onSignoutCallback = react.useCallback(() => {
167
850
  log("info", "Sign out successful");
851
+ window.history.replaceState({}, document.title, window.location.pathname);
168
852
  config.events?.onLogout?.();
169
853
  }, [config.events, log]);
170
854
  const onRemoveUser = react.useCallback(() => {
171
- log("info", "User removed");
855
+ log("warn", "User removed from storage - token validation likely failed");
172
856
  }, [log]);
857
+ const showSessionMonitor = config.features?.sessionMonitor !== false;
173
858
  return /* @__PURE__ */ jsxRuntime.jsx(
174
859
  reactOidcContext.AuthProvider,
175
860
  {
@@ -177,11 +862,26 @@ var KeycloakProvider = ({
177
862
  onSigninCallback,
178
863
  onSignoutCallback,
179
864
  onRemoveUser,
180
- children
865
+ children: /* @__PURE__ */ jsxRuntime.jsxs(TokenLifecycleManager, { config, debug, children: [
866
+ showSessionMonitor && /* @__PURE__ */ jsxRuntime.jsx(SessionMonitor, {}),
867
+ children
868
+ ] })
181
869
  }
182
870
  );
183
871
  };
184
- var useAuth = reactOidcContext.useAuth;
872
+ function buildZeroConfig(props) {
873
+ const { authority, clientId, preset = "default", config: overrides = {} } = props;
874
+ const presetConfig = mergeWithPreset(preset, overrides);
875
+ const fullConfig = {
876
+ ...defaultConfig,
877
+ ...presetConfig,
878
+ authority,
879
+ clientId,
880
+ redirectUri: presetConfig.redirectUri || (typeof window !== "undefined" ? window.location.origin + "/" : "/"),
881
+ postLogoutRedirectUri: presetConfig.postLogoutRedirectUri || (typeof window !== "undefined" ? window.location.origin + "/" : "/")
882
+ };
883
+ return fullConfig;
884
+ }
185
885
  function useRoles() {
186
886
  const auth = useAuth();
187
887
  const roles = react.useMemo(() => {
@@ -509,125 +1209,6 @@ function getInitials(name) {
509
1209
  }
510
1210
  return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
511
1211
  }
512
- var SessionMonitor = ({
513
- position = "top-right",
514
- warningThreshold = 300,
515
- // 5 minutes
516
- criticalThreshold = 60,
517
- // 1 minute
518
- showProgressBar = true,
519
- minimizable = true,
520
- startMinimized = false,
521
- className = "",
522
- onWarning,
523
- onCritical,
524
- onExpired,
525
- style
526
- }) => {
527
- const auth = useAuth();
528
- const [timeRemaining, setTimeRemaining] = react.useState(null);
529
- const [minimized, setMinimized] = react.useState(startMinimized);
530
- const [warningTriggered, setWarningTriggered] = react.useState(false);
531
- const [criticalTriggered, setCriticalTriggered] = react.useState(false);
532
- react.useEffect(() => {
533
- if (!auth.isAuthenticated || !auth.user?.expires_at) {
534
- setTimeRemaining(null);
535
- return;
536
- }
537
- const updateTimeRemaining = () => {
538
- const expiresAt = auth.user.expires_at;
539
- const now = Math.floor(Date.now() / 1e3);
540
- const remaining = expiresAt - now;
541
- if (remaining <= 0) {
542
- setTimeRemaining(0);
543
- onExpired?.();
544
- setTimeout(() => {
545
- auth.signoutRedirect();
546
- }, 2e3);
547
- } else {
548
- setTimeRemaining(remaining);
549
- if (remaining <= criticalThreshold && !criticalTriggered) {
550
- setCriticalTriggered(true);
551
- onCritical?.(remaining);
552
- } else if (remaining <= warningThreshold && !warningTriggered) {
553
- setWarningTriggered(true);
554
- onWarning?.(remaining);
555
- }
556
- }
557
- };
558
- updateTimeRemaining();
559
- const interval = setInterval(updateTimeRemaining, 1e3);
560
- return () => clearInterval(interval);
561
- }, [
562
- auth,
563
- warningThreshold,
564
- criticalThreshold,
565
- warningTriggered,
566
- criticalTriggered,
567
- onWarning,
568
- onCritical,
569
- onExpired
570
- ]);
571
- react.useEffect(() => {
572
- if (timeRemaining && timeRemaining > warningThreshold) {
573
- setWarningTriggered(false);
574
- setCriticalTriggered(false);
575
- }
576
- }, [timeRemaining, warningThreshold]);
577
- const formatTime = react.useCallback((seconds) => {
578
- const mins = Math.floor(seconds / 60);
579
- const secs = seconds % 60;
580
- return `${mins}:${secs.toString().padStart(2, "0")}`;
581
- }, []);
582
- const getStatusClass = react.useCallback(() => {
583
- if (!timeRemaining) return "normal";
584
- if (timeRemaining <= criticalThreshold) return "critical";
585
- if (timeRemaining <= warningThreshold) return "warning";
586
- return "normal";
587
- }, [timeRemaining, warningThreshold, criticalThreshold]);
588
- const getProgress = react.useCallback(() => {
589
- if (!timeRemaining || !auth.user?.expires_in) return 100;
590
- return timeRemaining / auth.user.expires_in * 100;
591
- }, [timeRemaining, auth.user]);
592
- if (!auth.isAuthenticated || timeRemaining === null) {
593
- return null;
594
- }
595
- const baseClass = "kc-session-monitor";
596
- const positionClass = `kc-session-monitor--${position}`;
597
- const statusClass = `kc-session-monitor--${getStatusClass()}`;
598
- const minimizedClass = minimized ? "kc-session-monitor--minimized" : "";
599
- const classes = `${baseClass} ${positionClass} ${statusClass} ${minimizedClass} ${className}`.trim();
600
- const progress = getProgress();
601
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: classes, style, children: [
602
- minimizable && /* @__PURE__ */ jsxRuntime.jsx(
603
- "button",
604
- {
605
- className: "kc-session-monitor__toggle",
606
- onClick: () => setMinimized(!minimized),
607
- "aria-label": minimized ? "Expand session monitor" : "Minimize session monitor",
608
- title: minimized ? "Expand" : "Minimize",
609
- children: minimized ? "\u25B2" : "\u25BC"
610
- }
611
- ),
612
- !minimized && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
613
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kc-session-monitor__content", children: [
614
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-session-monitor__label", children: timeRemaining === 0 ? "Session expired" : "Session expires in:" }),
615
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kc-session-monitor__time", children: formatTime(timeRemaining) })
616
- ] }),
617
- showProgressBar && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kc-session-monitor__progress-bar", children: /* @__PURE__ */ jsxRuntime.jsx(
618
- "div",
619
- {
620
- className: "kc-session-monitor__progress-fill",
621
- style: { width: `${progress}%` },
622
- role: "progressbar",
623
- "aria-valuenow": progress,
624
- "aria-valuemin": 0,
625
- "aria-valuemax": 100
626
- }
627
- ) })
628
- ] })
629
- ] });
630
- };
631
1212
 
632
1213
  // src/utils/deviceFlow.ts
633
1214
  async function requestDeviceCode(apiBaseUrl) {
@@ -935,6 +1516,7 @@ function useProtectedFetch(options = {}) {
935
1516
  };
936
1517
  }
937
1518
 
1519
+ exports.CONFIG_PRESETS = CONFIG_PRESETS;
938
1520
  exports.DeviceLogin = DeviceLogin;
939
1521
  exports.KeycloakConfigBuilder = KeycloakConfigBuilder;
940
1522
  exports.KeycloakProvider = KeycloakProvider;
@@ -942,15 +1524,19 @@ exports.LoginButton = LoginButton;
942
1524
  exports.LogoutButton = LogoutButton;
943
1525
  exports.ProtectedRoute = ProtectedRoute;
944
1526
  exports.SessionMonitor = SessionMonitor;
1527
+ exports.TokenLifecycleManager = TokenLifecycleManager;
945
1528
  exports.UserProfile = UserProfile;
946
1529
  exports.defaultConfig = defaultConfig;
947
1530
  exports.fetchUserInfo = fetchUserInfo;
948
1531
  exports.getConfigFromEnv = getConfigFromEnv;
1532
+ exports.getConfigPreset = getConfigPreset;
1533
+ exports.mergeWithPreset = mergeWithPreset;
949
1534
  exports.pollForToken = pollForToken;
950
1535
  exports.requestDeviceCode = requestDeviceCode;
951
1536
  exports.useAuth = useAuth;
952
1537
  exports.useProtectedFetch = useProtectedFetch;
953
1538
  exports.useRoles = useRoles;
954
1539
  exports.useScopes = useScopes;
1540
+ exports.useTokenLifecycle = useTokenLifecycle;
955
1541
  //# sourceMappingURL=index.js.map
956
1542
  //# sourceMappingURL=index.js.map