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