tenneo-auth-plugin 0.1.11 → 1.0.2

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
@@ -21,64 +21,6 @@ function getAuthRuntime() {
21
21
  function isLocalhost(hostname) {
22
22
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.endsWith(".local");
23
23
  }
24
- function extractTenantKeyFromDomain(urlOrHostname, config) {
25
- try {
26
- let hostname;
27
- if (urlOrHostname.startsWith("http://") || urlOrHostname.startsWith("https://")) {
28
- try {
29
- const url = new URL(urlOrHostname);
30
- hostname = url.hostname;
31
- } catch {
32
- hostname = urlOrHostname.replace(/^https?:\/\//, "").split("/")[0];
33
- }
34
- } else {
35
- hostname = urlOrHostname;
36
- }
37
- const parts = hostname.split(".");
38
- if (parts.length < 2) return config.getClientCode();
39
- const tenantKey = parts[0];
40
- if (!tenantKey || tenantKey.trim() === "") return config.getClientCode();
41
- if (tenantKey.trim().toLowerCase() === "productlms") return config.getClientCode();
42
- return tenantKey.trim();
43
- } catch {
44
- return config.getClientCode();
45
- }
46
- }
47
- function extractTenantKeyFromUrl(url, config, getStorage3, getKey) {
48
- try {
49
- const propsConfigKey = getKey ? getKey("props_config") : "tenneo_auth_props_config";
50
- const propsConfigStr = getStorage3(propsConfigKey);
51
- if (propsConfigStr) {
52
- try {
53
- const propsConfig = JSON.parse(propsConfigStr);
54
- if (propsConfig.tenantKey?.trim()) return propsConfig.tenantKey.trim();
55
- } catch {
56
- }
57
- }
58
- const tenantKeyStorageKey = getKey ? getKey("tenant_key") : "tenneo_auth_tenant_key";
59
- const storedTenantKey = getStorage3(tenantKeyStorageKey);
60
- if (storedTenantKey?.trim()) return storedTenantKey.trim();
61
- const urlParams = new URLSearchParams(url.search);
62
- const tenantKeyFromQuery = urlParams.get("t");
63
- if (tenantKeyFromQuery?.trim()) {
64
- const hostnameParts = url.hostname.split(".");
65
- const baseDomain = hostnameParts.length >= 2 ? hostnameParts.slice(-2).join(".") : url.hostname;
66
- const simulatedHostname = `${tenantKeyFromQuery.trim()}.${baseDomain}`;
67
- const key = extractTenantKeyFromDomain(simulatedHostname, config);
68
- if (key) return key;
69
- return tenantKeyFromQuery.trim();
70
- }
71
- const keyFromHost = extractTenantKeyFromDomain(url.hostname, config);
72
- if (keyFromHost) return keyFromHost;
73
- const clientCode = config.getClientCode();
74
- if (clientCode?.trim()) return clientCode.trim();
75
- return config.getClientCode();
76
- } catch {
77
- const clientCode = config.getClientCode();
78
- if (clientCode?.trim()) return clientCode.trim();
79
- return config.getClientCode();
80
- }
81
- }
82
24
  function isCallbackUrlFromUrl(url) {
83
25
  const params = new URLSearchParams(url.search);
84
26
  return params.has("code") && params.has("state");
@@ -139,8 +81,7 @@ var CONFIG_KEY_SUFFIXES = {
139
81
  stateMismatch: "state_mismatch",
140
82
  bootstrapInProgress: "bootstrap_in_progress",
141
83
  bootstrapTimestamp: "bootstrap_timestamp",
142
- callbackHandled: "callback_handled",
143
- idToken: "id_token"
84
+ callbackHandled: "callback_handled"
144
85
  };
145
86
 
146
87
  // src/infrastructure/storage/context.ts
@@ -434,10 +375,6 @@ function createCookieStorageAdapter(namespace, options) {
434
375
 
435
376
  // src/infrastructure/storage/api.ts
436
377
  var KEY_SUFFIXES = {
437
- authServerUrl: "auth_server_url",
438
- clientId: "client_id",
439
- redirectUri: "redirect_uri",
440
- tenantId: "tenant_id",
441
378
  tenantKey: "tenant_key",
442
379
  codeVerifier: "code_verifier",
443
380
  state: "state",
@@ -446,18 +383,6 @@ var KEY_SUFFIXES = {
446
383
  expiresAt: "expires_at"
447
384
  };
448
385
  var STORAGE_KEYS = {
449
- get authServerUrl() {
450
- return getStorageKey(KEY_SUFFIXES.authServerUrl);
451
- },
452
- get clientId() {
453
- return getStorageKey(KEY_SUFFIXES.clientId);
454
- },
455
- get redirectUri() {
456
- return getStorageKey(KEY_SUFFIXES.redirectUri);
457
- },
458
- get tenantId() {
459
- return getStorageKey(KEY_SUFFIXES.tenantId);
460
- },
461
386
  get tenantKey() {
462
387
  return getStorageKey(KEY_SUFFIXES.tenantKey);
463
388
  },
@@ -513,22 +438,22 @@ function removeStorage(key) {
513
438
  }
514
439
  }
515
440
  function clearAuthStorage() {
516
- const adapter = getStorageAdapter();
517
- if (!adapter?.clearSync) return;
518
- try {
519
- adapter.clearTokensSync?.();
520
- adapter.clearSync();
521
- } catch {
522
- }
441
+ const keysToClear = [
442
+ STORAGE_KEYS.accessToken,
443
+ STORAGE_KEYS.refreshToken,
444
+ STORAGE_KEYS.expiresAt,
445
+ STORAGE_KEYS.codeVerifier,
446
+ STORAGE_KEYS.state,
447
+ getStorageKey("callback_processed"),
448
+ getStorageKey("props_config"),
449
+ getStorageKey("tenant_switched"),
450
+ getStorageKey("new_tenant_key"),
451
+ getStorageKey("preserved_tenant_key")
452
+ ];
453
+ for (const key of keysToClear) removeStorage(key);
523
454
  }
524
455
  function clearAuthSessionStorage() {
525
- const adapter = getStorageAdapter();
526
- if (!adapter?.clearSync) return;
527
- try {
528
- adapter.clearTokensSync?.();
529
- adapter.clearSync();
530
- } catch {
531
- }
456
+ clearAuthStorage();
532
457
  }
533
458
  function clearAllCookies() {
534
459
  const runtime = getAuthRuntime();
@@ -537,27 +462,8 @@ function clearAllCookies() {
537
462
  } catch {
538
463
  }
539
464
  }
540
- function clearAllBrowserStorage() {
541
- if (typeof window === "undefined") return;
542
- try {
543
- window.localStorage?.clear();
544
- } catch {
545
- }
546
- try {
547
- window.sessionStorage?.clear();
548
- } catch {
549
- }
550
- }
551
465
  function clearAllStorage() {
552
- const forceLogin = getStorage(getStorageKey("force_login"));
553
- const logoutInProgress = getStorage(getStorageKey("logout_in_progress"));
554
- const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
555
- clearAuthSessionStorage();
556
466
  clearAuthStorage();
557
- clearAllBrowserStorage();
558
- if (forceLogin === "true") setStorage(getStorageKey("force_login"), "true");
559
- if (logoutInProgress === "true") setStorage(getStorageKey("logout_in_progress"), "true");
560
- if (preservedTenantKey) setStorage(getStorageKey("preserved_tenant_key"), preservedTenantKey);
561
467
  clearAllCookies();
562
468
  }
563
469
  function getAccessToken() {
@@ -593,14 +499,27 @@ function isAccessTokenExpired() {
593
499
  const buffer = 60 * 1e3;
594
500
  return Date.now() >= expiresAt - buffer;
595
501
  }
502
+ function readHostConfig() {
503
+ try {
504
+ const raw = getStorage(getStorageKey("host_config"));
505
+ if (!raw) return {};
506
+ const parsed = JSON.parse(raw);
507
+ return parsed && typeof parsed === "object" ? parsed : {};
508
+ } catch {
509
+ return {};
510
+ }
511
+ }
596
512
  function getClientId() {
597
- return getStorage(STORAGE_KEYS.clientId);
513
+ const value = readHostConfig().clientId;
514
+ return typeof value === "string" && value.trim() ? value : null;
598
515
  }
599
516
  function getRedirectUri() {
600
- return getStorage(STORAGE_KEYS.redirectUri);
517
+ const value = readHostConfig().redirectUri;
518
+ return typeof value === "string" && value.trim() ? value : null;
601
519
  }
602
520
  function getTenantId() {
603
- return getStorage(STORAGE_KEYS.tenantId);
521
+ const value = readHostConfig().tenantId;
522
+ return typeof value === "string" && value.trim() ? value : null;
604
523
  }
605
524
  function getCodeVerifier() {
606
525
  return getStorage(STORAGE_KEYS.codeVerifier);
@@ -717,10 +636,8 @@ function resolveAuthLifecycleState(input) {
717
636
  hasRefreshToken,
718
637
  isTokenExpired
719
638
  } = input;
720
- const forceLogin = getStorage3("force_login") === "true";
721
639
  const callbackProcessed = getStorage3("callback_processed") === "true";
722
640
  const tenantSwitched = getStorage3("tenant_switched") === "true";
723
- if (forceLogin) return "FORCE_LOGIN" /* FORCE_LOGIN */;
724
641
  if (isCallbackUrl2(url)) return "PROCESSING_CALLBACK" /* PROCESSING_CALLBACK */;
725
642
  if (hasValidToken) return "AUTHENTICATED" /* AUTHENTICATED */;
726
643
  if (callbackProcessed && !hasToken) return "LOGIN_REQUIRED" /* LOGIN_REQUIRED */;
@@ -781,30 +698,58 @@ function setFlowFlags() {
781
698
  setFlowInProgress();
782
699
  }
783
700
 
784
- // src/strategies/oauth2-pkce/pkce.ts
785
- function generateCodeVerifier(length = 128) {
786
- const array = new Uint8Array(length);
787
- crypto.getRandomValues(array);
788
- const base64 = btoa(String.fromCharCode(...array));
789
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
701
+ // src/application/events/AuthEvents.ts
702
+ var AuthEventNames = {
703
+ LOGIN_STARTED: "LOGIN_STARTED",
704
+ LOGIN_SUCCESS: "LOGIN_SUCCESS",
705
+ LOGIN_FAILED: "LOGIN_FAILED",
706
+ CALLBACK_SUCCESS: "CALLBACK_SUCCESS",
707
+ CALLBACK_FAILED: "CALLBACK_FAILED",
708
+ TOKEN_REFRESH_SUCCESS: "TOKEN_REFRESH_SUCCESS",
709
+ TOKEN_REFRESH_FAILED: "TOKEN_REFRESH_FAILED",
710
+ TENANT_SWITCHED: "TENANT_SWITCHED",
711
+ LOGOUT_COMPLETED: "LOGOUT_COMPLETED",
712
+ BOOTSTRAP_STATE_DETECTED: "BOOTSTRAP_STATE_DETECTED",
713
+ REDIRECT_LOOP_RISK: "REDIRECT_LOOP_RISK"
714
+ };
715
+ var listeners = /* @__PURE__ */ new Map();
716
+ function getHandlers(event) {
717
+ let set = listeners.get(event);
718
+ if (!set) {
719
+ set = /* @__PURE__ */ new Set();
720
+ listeners.set(event, set);
721
+ }
722
+ return set;
790
723
  }
791
- async function generateCodeChallenge(verifier) {
792
- try {
793
- const encoder = new TextEncoder();
794
- const data = encoder.encode(verifier);
795
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
796
- const hashArray = Array.from(new Uint8Array(hashBuffer));
797
- const base64 = btoa(String.fromCharCode(...hashArray));
798
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
799
- } catch {
800
- throw new Error("Failed to generate code challenge");
724
+ function getWildcardHandlers() {
725
+ let set = listeners.get("*");
726
+ if (!set) {
727
+ set = /* @__PURE__ */ new Set();
728
+ listeners.set("*", set);
801
729
  }
730
+ return set;
802
731
  }
803
- function generateState() {
804
- const array = new Uint8Array(32);
805
- crypto.getRandomValues(array);
806
- const base64 = btoa(String.fromCharCode(...array));
807
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
732
+ function emitAuthEvent(event, payload) {
733
+ const handlers = getHandlers(event);
734
+ const wildcard = getWildcardHandlers();
735
+ const p = payload ?? {};
736
+ handlers.forEach((fn) => {
737
+ try {
738
+ fn(p);
739
+ } catch {
740
+ }
741
+ });
742
+ wildcard.forEach((fn) => {
743
+ try {
744
+ fn({ ...p, event });
745
+ } catch {
746
+ }
747
+ });
748
+ }
749
+ function subscribeAuthEvent(event, handler) {
750
+ const set = event === "*" ? getWildcardHandlers() : getHandlers(event);
751
+ set.add(handler);
752
+ return () => set.delete(handler);
808
753
  }
809
754
 
810
755
  // src/config/constants.ts
@@ -848,12 +793,17 @@ function readHostConfigFromStorage() {
848
793
  function resolveMerged(overrides) {
849
794
  const stored = readHostConfigFromStorage();
850
795
  const cfg = { ...stored, ...inMemoryConfig, ...overrides ?? {} };
796
+ if (!cfg.tenantCode && cfg.tenantKey) {
797
+ cfg.tenantCode = cfg.tenantKey;
798
+ }
851
799
  const trimmed = (v) => v == null ? "" : String(v).trim();
852
800
  return {
853
801
  apiBaseUrl: trimmed(cfg.apiBaseUrl),
854
802
  authServerUrl: trimmed(cfg.authServerUrl),
855
803
  callbackUrl: trimmed(cfg.callbackUrl),
856
- clientCode: trimmed(cfg.clientCode)
804
+ clientCode: trimmed(cfg.clientCode),
805
+ tenantCode: trimmed(cfg.tenantCode),
806
+ appId: trimmed(cfg.appId)
857
807
  };
858
808
  }
859
809
  function resolveConfig(overrides) {
@@ -862,6 +812,9 @@ function resolveConfig(overrides) {
862
812
  function getConfig() {
863
813
  return resolveMerged();
864
814
  }
815
+ function getAuthConfig() {
816
+ return resolveMerged();
817
+ }
865
818
  function setConfig(overrides) {
866
819
  inMemoryConfig = { ...inMemoryConfig, ...overrides };
867
820
  }
@@ -892,28 +845,121 @@ var Config = {
892
845
  }
893
846
  };
894
847
 
895
- // src/utils/validation.ts
896
- function isValidGuid(guid) {
897
- if (!guid || typeof guid !== "string") {
898
- return false;
848
+ // src/infrastructure/tenant/TenantConfigCache.ts
849
+ var DEFAULT_TTL_MS = 5 * 60 * 1e3;
850
+ var TenantConfigCache = class {
851
+ constructor(ttlMs = DEFAULT_TTL_MS) {
852
+ this.cache = /* @__PURE__ */ new Map();
853
+ this.inFlight = /* @__PURE__ */ new Map();
854
+ this.ttlMs = ttlMs;
855
+ }
856
+ get(tenantKey) {
857
+ const entry = this.cache.get(tenantKey);
858
+ if (!entry) return null;
859
+ if (Date.now() >= entry.expiresAt) {
860
+ this.cache.delete(tenantKey);
861
+ return null;
862
+ }
863
+ return entry.config;
864
+ }
865
+ set(tenantKey, config) {
866
+ this.cache.set(tenantKey, {
867
+ config,
868
+ expiresAt: Date.now() + this.ttlMs
869
+ });
870
+ }
871
+ invalidate(tenantKey) {
872
+ if (tenantKey) {
873
+ this.cache.delete(tenantKey);
874
+ this.inFlight.delete(tenantKey);
875
+ } else {
876
+ this.cache.clear();
877
+ this.inFlight.clear();
878
+ }
879
+ }
880
+ async getOrResolve(tenantKey, resolver) {
881
+ if (!tenantKey || !tenantKey.trim()) return null;
882
+ const key = tenantKey.trim();
883
+ const cached = this.get(key);
884
+ if (cached) {
885
+ logger.debug("Tenant config from cache", { tenantKey: key });
886
+ return cached;
887
+ }
888
+ const existing = this.inFlight.get(key);
889
+ if (existing) {
890
+ logger.debug("Tenant resolution reused (in-flight)", { tenantKey: key });
891
+ return existing;
892
+ }
893
+ const promise = (async () => {
894
+ try {
895
+ const config = await resolver(key);
896
+ if (config) this.set(key, config);
897
+ return config;
898
+ } finally {
899
+ this.inFlight.delete(key);
900
+ }
901
+ })();
902
+ this.inFlight.set(key, promise);
903
+ return promise;
904
+ }
905
+ };
906
+
907
+ // src/infrastructure/tenant/tenantResolution.ts
908
+ var defaultTenantConfigCache = new TenantConfigCache();
909
+ function consumePropsConfig() {
910
+ const propsConfigKey = getStorageKey("props_config");
911
+ try {
912
+ const propsConfigStr = getStorage(propsConfigKey);
913
+ if (!propsConfigStr) return null;
914
+ const parsed = JSON.parse(propsConfigStr);
915
+ return parsed && typeof parsed === "object" ? parsed : null;
916
+ } catch {
917
+ return null;
918
+ } finally {
919
+ removeStorage(propsConfigKey);
899
920
  }
900
- const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
901
- return guidRegex.test(guid.trim());
902
921
  }
903
- function validateTenantId(tenantId) {
904
- if (!tenantId || tenantId.trim() === "") {
905
- return {
906
- isValid: false,
907
- error: "Tenant ID is required"
908
- };
922
+ function readHostConfig2() {
923
+ try {
924
+ const raw = getStorage(getStorageKey("host_config"));
925
+ if (!raw) return null;
926
+ const parsed = JSON.parse(raw);
927
+ return parsed && typeof parsed === "object" ? parsed : null;
928
+ } catch {
929
+ return null;
909
930
  }
910
- if (!isValidGuid(tenantId)) {
911
- return {
912
- isValid: false,
913
- error: "Tenant ID must be a valid GUID format (00000000-0000-0000-0000-000000000001)"
914
- };
931
+ }
932
+ function validateTenantKey(value) {
933
+ if (typeof value !== "string") return null;
934
+ const trimmed = value.trim();
935
+ return trimmed ? trimmed : null;
936
+ }
937
+ function resolveTenantKey() {
938
+ const propsConfig = consumePropsConfig();
939
+ const tenantFromProps = validateTenantKey(propsConfig?.tenantKey);
940
+ if (tenantFromProps) return tenantFromProps;
941
+ const hostConfig = readHostConfig2();
942
+ if (!hostConfig) {
943
+ logger.warn("Missing host_config while resolving tenant");
944
+ return null;
915
945
  }
916
- return { isValid: true };
946
+ const tenantFromHost = validateTenantKey(hostConfig?.tenantKey);
947
+ if (tenantFromHost) return tenantFromHost;
948
+ if (hostConfig.tenantKey != null) {
949
+ logger.warn("Invalid tenant key in host_config", { tenantKey: hostConfig.tenantKey });
950
+ }
951
+ return null;
952
+ }
953
+ async function resolveTenantConfig(tenantKey, tenantResolver) {
954
+ const resolved = await tenantResolver.resolve(tenantKey);
955
+ if (!resolved) return null;
956
+ return { ...resolved, tenantKey };
957
+ }
958
+ async function resolveTenantConfigCached(tenantKey, tenantResolver) {
959
+ return defaultTenantConfigCache.getOrResolve(
960
+ tenantKey,
961
+ (key) => resolveTenantConfig(key, tenantResolver)
962
+ );
917
963
  }
918
964
 
919
965
  // src/application/domain-facade.ts
@@ -923,33 +969,7 @@ function isLocalhost2() {
923
969
  return isLocalhost(hostname);
924
970
  }
925
971
  function extractTenantKey() {
926
- const runtime = getAuthRuntime();
927
- if (runtime) {
928
- const url = runtime.urlProvider.getCurrentUrl();
929
- return extractTenantKeyFromUrl(url, runtime.configProvider, getStorage, getStorageKey);
930
- }
931
- return extractTenantKeyLegacy();
932
- }
933
- function extractTenantKeyLegacy() {
934
- if (typeof window === "undefined") return Config.getClientCode();
935
- try {
936
- const propsConfigStr = getStorage(getStorageKey("props_config"));
937
- if (propsConfigStr) {
938
- const propsConfig = JSON.parse(propsConfigStr);
939
- if (propsConfig.tenantKey?.trim()) return propsConfig.tenantKey.trim();
940
- }
941
- const stored = getStorage(getStorageKey("tenant_key"));
942
- if (stored?.trim()) return stored.trim();
943
- const urlParams = new URLSearchParams(window.location.search);
944
- const t = urlParams.get("t");
945
- if (t?.trim()) return t.trim();
946
- const hostname = window.location.hostname;
947
- const parts = hostname.split(".");
948
- if (parts.length >= 2 && parts[0] && parts[0].toLowerCase() !== "productlms") return parts[0].trim();
949
- return Config.getClientCode();
950
- } catch {
951
- return Config.getClientCode();
952
- }
972
+ return resolveTenantKey();
953
973
  }
954
974
  function isCallbackUrl() {
955
975
  const runtime = getAuthRuntime();
@@ -959,219 +979,6 @@ function isCallbackUrl() {
959
979
  return params.has("code") && params.has("state");
960
980
  }
961
981
 
962
- // src/application/events/AuthEvents.ts
963
- var AuthEventNames = {
964
- LOGIN_STARTED: "LOGIN_STARTED",
965
- LOGIN_SUCCESS: "LOGIN_SUCCESS",
966
- LOGIN_FAILED: "LOGIN_FAILED",
967
- CALLBACK_SUCCESS: "CALLBACK_SUCCESS",
968
- CALLBACK_FAILED: "CALLBACK_FAILED",
969
- TOKEN_REFRESH_SUCCESS: "TOKEN_REFRESH_SUCCESS",
970
- TOKEN_REFRESH_FAILED: "TOKEN_REFRESH_FAILED",
971
- TENANT_SWITCHED: "TENANT_SWITCHED",
972
- LOGOUT_COMPLETED: "LOGOUT_COMPLETED",
973
- BOOTSTRAP_STATE_DETECTED: "BOOTSTRAP_STATE_DETECTED",
974
- REDIRECT_LOOP_RISK: "REDIRECT_LOOP_RISK"
975
- };
976
- var listeners = /* @__PURE__ */ new Map();
977
- function getHandlers(event) {
978
- let set = listeners.get(event);
979
- if (!set) {
980
- set = /* @__PURE__ */ new Set();
981
- listeners.set(event, set);
982
- }
983
- return set;
984
- }
985
- function getWildcardHandlers() {
986
- let set = listeners.get("*");
987
- if (!set) {
988
- set = /* @__PURE__ */ new Set();
989
- listeners.set("*", set);
990
- }
991
- return set;
992
- }
993
- function emitAuthEvent(event, payload) {
994
- const handlers = getHandlers(event);
995
- const wildcard = getWildcardHandlers();
996
- const p = payload ?? {};
997
- handlers.forEach((fn) => {
998
- try {
999
- fn(p);
1000
- } catch {
1001
- }
1002
- });
1003
- wildcard.forEach((fn) => {
1004
- try {
1005
- fn({ ...p, event });
1006
- } catch {
1007
- }
1008
- });
1009
- }
1010
- function subscribeAuthEvent(event, handler) {
1011
- const set = event === "*" ? getWildcardHandlers() : getHandlers(event);
1012
- set.add(handler);
1013
- return () => set.delete(handler);
1014
- }
1015
-
1016
- // src/application/security/redirectLoopGuard.ts
1017
- var DEFAULT_MAX_REDIRECTS = 5;
1018
- var WINDOW_MS = 60 * 1e3;
1019
- var redirectCount = 0;
1020
- var firstRedirectAt = 0;
1021
- function resetRedirectLoopGuard() {
1022
- redirectCount = 0;
1023
- firstRedirectAt = 0;
1024
- }
1025
- function checkAndIncrementRedirect(maxAttempts = DEFAULT_MAX_REDIRECTS) {
1026
- const now = Date.now();
1027
- if (now - firstRedirectAt > WINDOW_MS) {
1028
- redirectCount = 0;
1029
- firstRedirectAt = now;
1030
- }
1031
- if (redirectCount >= maxAttempts) {
1032
- emitAuthEvent(AuthEventNames.REDIRECT_LOOP_RISK, {
1033
- message: `Redirect attempt limit (${maxAttempts}) exceeded`
1034
- });
1035
- return false;
1036
- }
1037
- redirectCount++;
1038
- return true;
1039
- }
1040
-
1041
- // src/strategies/oauth2-pkce/oauth.ts
1042
- function storeTenantConfig(config) {
1043
- const validation = validateTenantId(config.tenantId);
1044
- if (!validation.isValid) {
1045
- logger.warn("Tenant ID validation failed", { tenantId: config.tenantId });
1046
- }
1047
- const authServerToStore = Config.getAuthServerUrl() || config.authServer;
1048
- setStorage(STORAGE_KEYS.authServerUrl, authServerToStore);
1049
- setStorage(STORAGE_KEYS.clientId, config.client.clientId);
1050
- setStorage(STORAGE_KEYS.redirectUri, Config.getRedirectUri());
1051
- setStorage(STORAGE_KEYS.tenantId, config.tenantId);
1052
- if (config.tenantKey) {
1053
- setStorage(STORAGE_KEYS.tenantKey, config.tenantKey);
1054
- }
1055
- }
1056
- async function initiateAuthFlow(config) {
1057
- try {
1058
- logger.info("Initiating OAuth authorization flow");
1059
- const authServerFromProps = Config.getAuthServerUrl();
1060
- const authServerEffective = (authServerFromProps || config?.authServer || "").replace(/\/+$/, "");
1061
- const redirectUriEffective = Config.getRedirectUri();
1062
- const storedTenantId = getStorage(STORAGE_KEYS.tenantId);
1063
- const storedClientId = getStorage(STORAGE_KEYS.clientId);
1064
- const tenantIdEffective = storedTenantId || config?.tenantId || "";
1065
- const clientIdEffective = storedClientId || config?.client?.clientId || "";
1066
- logger.debug("[initiateAuthFlow] Resolved inputs", {
1067
- authServerFromProps: authServerFromProps ? "(provided)" : "(empty)",
1068
- authServerEffective,
1069
- redirectUriEffective,
1070
- tenantIdEffective,
1071
- clientIdEffective,
1072
- scopes: config?.client?.scopes,
1073
- hasRuntime: !!getAuthRuntime()
1074
- });
1075
- const token = getAccessToken();
1076
- if (token && !isAccessTokenExpired()) {
1077
- logger.debug("Valid token already exists, skipping auth flow");
1078
- clearFlowFlags();
1079
- return;
1080
- }
1081
- if (isCallbackUrl()) {
1082
- logger.debug("Callback URL detected - clearing flow flag to allow callback processing");
1083
- clearFlowFlags();
1084
- return;
1085
- }
1086
- if (isFlowInProgress()) {
1087
- if (isFlowFlagStale(3e3)) {
1088
- logger.info("Stale auth flow flag detected, clearing and proceeding");
1089
- clearFlowFlags();
1090
- } else {
1091
- logger.debug("Auth flow already in progress (recent), skipping duplicate redirect");
1092
- return;
1093
- }
1094
- }
1095
- setFlowFlags();
1096
- if (!(Config.getAuthServerUrl() || config?.authServer) || !config.client?.clientId || !config.client?.redirectUri) {
1097
- logger.error("[initiateAuthFlow] Missing required tenant config", {
1098
- authServerEffective,
1099
- clientId: config?.client?.clientId,
1100
- redirectUriFromTenantApi: config?.client?.redirectUri,
1101
- redirectUriEffective
1102
- });
1103
- throw new Error("Invalid tenant configuration for auth flow");
1104
- }
1105
- storeTenantConfig(config);
1106
- logger.debug("[initiateAuthFlow] Stored tenant config, generating PKCE");
1107
- const codeVerifier = generateCodeVerifier();
1108
- const codeChallenge = await generateCodeChallenge(codeVerifier);
1109
- const state = generateState();
1110
- removeStorage(STORAGE_KEYS.codeVerifier);
1111
- removeStorage(STORAGE_KEYS.state);
1112
- setStorage(STORAGE_KEYS.codeVerifier, codeVerifier);
1113
- setStorage(STORAGE_KEYS.state, state);
1114
- const authUrl = buildAuthorizationUrl(config, codeChallenge, state);
1115
- logger.debug("[initiateAuthFlow] Built authorization URL", {
1116
- authServerEffective,
1117
- urlPrefix: authUrl?.slice(0, 80)
1118
- });
1119
- if (!authUrl || !(authUrl.startsWith("http://") || authUrl.startsWith("https://"))) {
1120
- throw new Error("Invalid authorization URL generated");
1121
- }
1122
- if (!checkAndIncrementRedirect()) {
1123
- logger.warn("Redirect loop protection: max redirect attempts exceeded");
1124
- clearFlowFlags();
1125
- return;
1126
- }
1127
- logger.info("Redirecting to authorization server");
1128
- emitAuthEvent(AuthEventNames.LOGIN_STARTED, { tenantKey: config.tenantKey });
1129
- const runtime = getAuthRuntime();
1130
- if (runtime) {
1131
- logger.debug("[initiateAuthFlow] Redirect via runtime.urlProvider.redirect");
1132
- runtime.urlProvider.redirect(authUrl);
1133
- } else if (typeof window !== "undefined") {
1134
- logger.debug("[initiateAuthFlow] Redirect via window.location.replace");
1135
- window.location.replace(authUrl);
1136
- } else {
1137
- logger.error("[initiateAuthFlow] No runtime and no window; cannot redirect", { authUrl });
1138
- }
1139
- } catch (error) {
1140
- clearFlowFlags();
1141
- logger.error("Failed to initiate auth flow", {
1142
- error: error instanceof Error ? error.message : String(error)
1143
- });
1144
- throw error;
1145
- }
1146
- }
1147
- function buildAuthorizationUrl(config, codeChallenge, state) {
1148
- const authServerBase = (Config.getAuthServerUrl() || config.authServer || "").replace(/\/+$/, "");
1149
- if (isLocalhost2()) {
1150
- logger.debug("Building authorization URL", {
1151
- authServer: authServerBase,
1152
- clientId: config.client.clientId,
1153
- redirectUri: config.client.redirectUri,
1154
- scopes: config.client.scopes
1155
- });
1156
- }
1157
- const storedTenantId = getStorage(STORAGE_KEYS.tenantId);
1158
- const storedClientId = getStorage(STORAGE_KEYS.clientId);
1159
- const tenantIdToUse = storedTenantId || config.tenantId;
1160
- const clientIdToUse = storedClientId || config.client.clientId;
1161
- const redirectUriToUse = Config.getRedirectUri();
1162
- const params = new URLSearchParams({
1163
- client_id: `public-${clientIdToUse}`,
1164
- tenant_id: tenantIdToUse,
1165
- redirect_uri: redirectUriToUse,
1166
- response_type: DEFAULT_RESPONSE_TYPE,
1167
- scope: config.client.scopes,
1168
- state,
1169
- code_challenge: codeChallenge,
1170
- code_challenge_method: DEFAULT_CODE_CHALLENGE_METHOD
1171
- });
1172
- return `${authServerBase}/connect/authorize?${params.toString()}`;
1173
- }
1174
-
1175
982
  // src/strategies/oauth2-pkce/token-exchange.ts
1176
983
  function storeTokens(tokenData) {
1177
984
  const expiresIn = tokenData.expires_in || 3600;
@@ -1331,6 +1138,31 @@ async function refreshAccessToken() {
1331
1138
  })();
1332
1139
  return refreshPromise;
1333
1140
  }
1141
+
1142
+ // src/application/security/redirectLoopGuard.ts
1143
+ var DEFAULT_MAX_REDIRECTS = 5;
1144
+ var WINDOW_MS = 60 * 1e3;
1145
+ var redirectCount = 0;
1146
+ var firstRedirectAt = 0;
1147
+ function resetRedirectLoopGuard() {
1148
+ redirectCount = 0;
1149
+ firstRedirectAt = 0;
1150
+ }
1151
+ function checkAndIncrementRedirect(maxAttempts = DEFAULT_MAX_REDIRECTS) {
1152
+ const now = Date.now();
1153
+ if (now - firstRedirectAt > WINDOW_MS) {
1154
+ redirectCount = 0;
1155
+ firstRedirectAt = now;
1156
+ }
1157
+ if (redirectCount >= maxAttempts) {
1158
+ emitAuthEvent(AuthEventNames.REDIRECT_LOOP_RISK, {
1159
+ message: `Redirect attempt limit (${maxAttempts}) exceeded`
1160
+ });
1161
+ return false;
1162
+ }
1163
+ redirectCount++;
1164
+ return true;
1165
+ }
1334
1166
  function isProcessed() {
1335
1167
  return getStorage(getStorageKey("callback_processed")) === "true";
1336
1168
  }
@@ -1349,15 +1181,6 @@ function clearProcessing() {
1349
1181
  removeStorage(getStorageKey("callback_processed"));
1350
1182
  }
1351
1183
  var urlCleaned = false;
1352
- function cleanUrl() {
1353
- if (urlCleaned) return;
1354
- if (isProcessing()) return;
1355
- const runtime = getAuthRuntime();
1356
- const clean = runtime ? `${runtime.urlProvider.getCurrentUrl().origin}${runtime.urlProvider.getCurrentUrl().pathname}` : typeof window !== "undefined" ? `${window.location.origin}${window.location.pathname}` : "/";
1357
- if (runtime) runtime.urlProvider.redirect(clean);
1358
- else if (typeof window !== "undefined") window.location.replace(clean);
1359
- urlCleaned = true;
1360
- }
1361
1184
  function cleanUrlSoft() {
1362
1185
  if (urlCleaned) return;
1363
1186
  if (isProcessing()) return;
@@ -1368,13 +1191,20 @@ function cleanUrlSoft() {
1368
1191
  urlCleaned = true;
1369
1192
  }
1370
1193
  function clearAllAuthFlags() {
1371
- removeStorage(getStorageKey("force_login"));
1372
- removeStorage(getStorageKey("logout_in_progress"));
1373
1194
  clearFlowFlags();
1374
1195
  }
1375
1196
  async function handleCallback() {
1197
+ const runtimeSearch = getAuthRuntime()?.urlProvider.getCurrentUrl().search;
1198
+ const windowSearch = typeof window !== "undefined" ? window.location.search : "";
1199
+ logger.debug("[Callback] handleCallback invoked", {
1200
+ isProcessed: isProcessed(),
1201
+ isProcessing: isProcessing(),
1202
+ isCallbackUrl: isCallbackUrl(),
1203
+ runtimeSearch: runtimeSearch ?? "(none)",
1204
+ windowSearch: windowSearch || "(none)"
1205
+ });
1376
1206
  if (isProcessed()) {
1377
- logger.debug("Callback already processed in this session - preventing duplicate processing");
1207
+ logger.warn("[Callback] Early exit: callback already processed");
1378
1208
  if (isCallbackUrl()) {
1379
1209
  if (!isProcessing()) cleanUrlSoft();
1380
1210
  }
@@ -1382,31 +1212,16 @@ async function handleCallback() {
1382
1212
  return true;
1383
1213
  }
1384
1214
  if (isProcessing()) {
1385
- logger.debug("Callback processing already in progress - waiting for completion");
1215
+ logger.warn("[Callback] Early exit: callback processing already in progress");
1386
1216
  return true;
1387
1217
  }
1388
- if (getStorage(getStorageKey("force_login")) === "true") {
1389
- logger.info("Force-login flag present \u2192 callback skipped (logout occurred)");
1390
- const runtime = getAuthRuntime();
1391
- if (runtime && runtime.urlProvider.getCurrentUrl().search) cleanUrlSoft();
1392
- else if (typeof window !== "undefined" && window.location.search) cleanUrlSoft();
1393
- clearAllAuthFlags();
1394
- clearProcessing();
1395
- return false;
1396
- }
1397
1218
  const storedTenantKey = getTenantKey();
1398
- let currentTenantKey = null;
1399
- try {
1400
- const propsConfigStr = getStorage(getStorageKey("props_config"));
1401
- if (propsConfigStr) {
1402
- const propsConfig = JSON.parse(propsConfigStr);
1403
- currentTenantKey = propsConfig.tenantKey || null;
1404
- }
1405
- } catch {
1406
- }
1407
- if (!currentTenantKey) currentTenantKey = storedTenantKey;
1219
+ const currentTenantKey = resolveTenantKey() ?? storedTenantKey;
1408
1220
  if (currentTenantKey && storedTenantKey && currentTenantKey !== storedTenantKey) {
1409
- logger.info("TenantKey changed - bypassing callback, starting fresh login");
1221
+ logger.warn("[Callback] Early exit: tenant key changed before exchange", {
1222
+ currentTenantKey,
1223
+ storedTenantKey
1224
+ });
1410
1225
  clearAllStorage();
1411
1226
  clearAllAuthFlags();
1412
1227
  clearProcessing();
@@ -1416,7 +1231,10 @@ async function handleCallback() {
1416
1231
  if (isCallbackUrl()) cleanUrlSoft();
1417
1232
  return false;
1418
1233
  }
1419
- if (!isCallbackUrl()) return false;
1234
+ if (!isCallbackUrl()) {
1235
+ logger.warn("[Callback] Early exit: URL is not callback URL");
1236
+ return false;
1237
+ }
1420
1238
  markProcessing();
1421
1239
  clearFlowFlags();
1422
1240
  urlCleaned = false;
@@ -1427,44 +1245,33 @@ async function handleCallback() {
1427
1245
  const state = params.get("state");
1428
1246
  const error = params.get("error");
1429
1247
  if (error || !code || !state) {
1430
- logger.error("Invalid OAuth callback params", { error, code, state });
1248
+ logger.error("[Callback] Early exit: invalid OAuth callback params", { error, code, state });
1431
1249
  clearAllAuthFlags();
1432
1250
  clearProcessing();
1433
- markProcessed();
1434
1251
  cleanUrlSoft();
1435
1252
  return false;
1436
1253
  }
1437
1254
  const storedState = getState();
1438
1255
  if (!storedState || storedState !== state) {
1439
1256
  const currentTenantKeyFromUrl = extractTenantKey();
1440
- let storedTenantKeyFromProps = null;
1441
- try {
1442
- const propsConfigStr = getStorage(getStorageKey("props_config"));
1443
- if (propsConfigStr) {
1444
- const propsConfig = JSON.parse(propsConfigStr);
1445
- storedTenantKeyFromProps = propsConfig.tenantKey || null;
1446
- }
1447
- } catch {
1448
- }
1449
- const tenantKeyChanged = currentTenantKeyFromUrl && storedTenantKeyFromProps && currentTenantKeyFromUrl !== storedTenantKeyFromProps;
1257
+ const resolvedTenantKey = resolveTenantKey();
1258
+ const tenantKeyChanged = currentTenantKeyFromUrl && resolvedTenantKey && currentTenantKeyFromUrl !== resolvedTenantKey;
1450
1259
  if (tenantKeyChanged) {
1451
- logger.info("OAuth state mismatch due to tenantKey change - expected behavior, starting fresh login");
1260
+ logger.warn("[Callback] Early exit: state mismatch due to tenant key change");
1452
1261
  } else {
1453
- logger.error("OAuth state mismatch - redirecting to login");
1262
+ logger.error("[Callback] Early exit: OAuth state mismatch - redirecting to login");
1454
1263
  }
1455
1264
  clearAllAuthFlags();
1456
1265
  clearProcessing();
1457
- markProcessed();
1458
1266
  cleanUrlSoft();
1459
1267
  setStorage(getStorageKey("state_mismatch"), "true");
1460
1268
  return false;
1461
1269
  }
1462
1270
  const codeVerifier = getCodeVerifier();
1463
1271
  if (!codeVerifier) {
1464
- logger.error("Missing PKCE verifier");
1272
+ logger.error("[Callback] Early exit: missing PKCE verifier");
1465
1273
  clearAllAuthFlags();
1466
1274
  clearProcessing();
1467
- markProcessed();
1468
1275
  cleanUrlSoft();
1469
1276
  return false;
1470
1277
  }
@@ -1472,25 +1279,29 @@ async function handleCallback() {
1472
1279
  const redirectUri = getRedirectUri();
1473
1280
  const tenantId = getTenantId();
1474
1281
  if (!clientId || !redirectUri || !tenantId) {
1475
- logger.error("Missing OAuth config during callback");
1282
+ logger.error("[Callback] Early exit: missing OAuth config during callback", {
1283
+ hasClientId: !!clientId,
1284
+ hasRedirectUri: !!redirectUri,
1285
+ hasTenantId: !!tenantId
1286
+ });
1476
1287
  clearAllAuthFlags();
1477
1288
  clearProcessing();
1478
- markProcessed();
1479
1289
  cleanUrlSoft();
1480
1290
  return false;
1481
1291
  }
1482
1292
  const existingToken = getAccessToken();
1483
1293
  if (existingToken && !isAccessTokenExpired()) {
1484
- logger.debug("Valid token already exists - skipping code exchange");
1294
+ logger.warn("[Callback] Early exit: valid token already exists - skipping code exchange");
1485
1295
  removeStorage(STORAGE_KEYS.codeVerifier);
1486
1296
  removeStorage(STORAGE_KEYS.state);
1487
1297
  clearAllAuthFlags();
1488
1298
  markProcessed();
1489
- cleanUrl();
1299
+ cleanUrlSoft();
1490
1300
  resetRedirectLoopGuard();
1491
1301
  emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
1492
1302
  return true;
1493
1303
  }
1304
+ logger.info("[Callback] Entering token exchange");
1494
1305
  const tokenData = await exchangeCodeForTokens({
1495
1306
  code,
1496
1307
  codeVerifier,
@@ -1510,7 +1321,7 @@ async function handleCallback() {
1510
1321
  removeStorage(STORAGE_KEYS.state);
1511
1322
  clearAllAuthFlags();
1512
1323
  markProcessed();
1513
- cleanUrl();
1324
+ cleanUrlSoft();
1514
1325
  logger.info("OAuth callback processed successfully");
1515
1326
  resetRedirectLoopGuard();
1516
1327
  emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
@@ -1557,170 +1368,19 @@ async function handleCallback() {
1557
1368
  const tokenValid = existingToken && !isAccessTokenExpired();
1558
1369
  if (tokenValid) {
1559
1370
  markProcessed();
1560
- cleanUrlSoft();
1561
- resetRedirectLoopGuard();
1562
- emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
1563
- return true;
1564
- }
1565
- emitAuthEvent(AuthEventNames.CALLBACK_FAILED, {
1566
- error: err instanceof Error ? err.message : String(err)
1567
- });
1568
- clearProcessing();
1569
- if (isCallbackUrl()) cleanUrlSoft();
1570
- return false;
1571
- }
1572
- }
1573
-
1574
- // src/infrastructure/tenant/TenantConfigCache.ts
1575
- var DEFAULT_TTL_MS = 5 * 60 * 1e3;
1576
- var TenantConfigCache = class {
1577
- constructor(ttlMs = DEFAULT_TTL_MS) {
1578
- this.cache = /* @__PURE__ */ new Map();
1579
- this.inFlight = /* @__PURE__ */ new Map();
1580
- this.ttlMs = ttlMs;
1581
- }
1582
- get(tenantKey) {
1583
- const entry = this.cache.get(tenantKey);
1584
- if (!entry) return null;
1585
- if (Date.now() >= entry.expiresAt) {
1586
- this.cache.delete(tenantKey);
1587
- return null;
1588
- }
1589
- return entry.config;
1590
- }
1591
- set(tenantKey, config) {
1592
- this.cache.set(tenantKey, {
1593
- config,
1594
- expiresAt: Date.now() + this.ttlMs
1595
- });
1596
- }
1597
- invalidate(tenantKey) {
1598
- if (tenantKey) {
1599
- this.cache.delete(tenantKey);
1600
- this.inFlight.delete(tenantKey);
1601
- } else {
1602
- this.cache.clear();
1603
- this.inFlight.clear();
1604
- }
1605
- }
1606
- async getOrResolve(tenantKey, resolver) {
1607
- if (!tenantKey || !tenantKey.trim()) return null;
1608
- const key = tenantKey.trim();
1609
- const cached = this.get(key);
1610
- if (cached) {
1611
- logger.debug("Tenant config from cache", { tenantKey: key });
1612
- return cached;
1613
- }
1614
- const existing = this.inFlight.get(key);
1615
- if (existing) {
1616
- logger.debug("Tenant resolution reused (in-flight)", { tenantKey: key });
1617
- return existing;
1618
- }
1619
- const promise = (async () => {
1620
- try {
1621
- const config = await resolver(key);
1622
- if (config) this.set(key, config);
1623
- return config;
1624
- } finally {
1625
- this.inFlight.delete(key);
1626
- }
1627
- })();
1628
- this.inFlight.set(key, promise);
1629
- return promise;
1630
- }
1631
- };
1632
-
1633
- // src/infrastructure/tenant/tenantResolution.ts
1634
- var defaultTenantConfigCache = new TenantConfigCache();
1635
- function getTenantKeyFromRuntime(url, config) {
1636
- try {
1637
- const propsConfigStr = getStorage(getStorageKey("props_config"));
1638
- if (propsConfigStr) {
1639
- const propsConfig = JSON.parse(propsConfigStr);
1640
- if (propsConfig.tenantKey?.trim()) return propsConfig.tenantKey.trim();
1641
- return null;
1642
- }
1643
- } catch {
1644
- }
1645
- return extractTenantKeyFromUrl(url, config, getStorage, getStorageKey);
1646
- }
1647
- function getPropsConfig() {
1648
- try {
1649
- const propsConfigStr = getStorage(getStorageKey("props_config"));
1650
- if (!propsConfigStr) return null;
1651
- const parsed = JSON.parse(propsConfigStr);
1652
- if (parsed && typeof parsed === "object" && "authServerUrl" in parsed) {
1653
- delete parsed.authServerUrl;
1654
- }
1655
- return parsed;
1656
- } catch {
1657
- removeStorage(getStorageKey("props_config"));
1658
- return null;
1659
- }
1660
- }
1661
- function mergeConfigWithProps(apiEnvConfig, propsConfig, tenantKey) {
1662
- return {
1663
- tenantId: propsConfig.tenantId || apiEnvConfig.tenantId,
1664
- authServer: apiEnvConfig.authServer,
1665
- tenantKey: tenantKey || apiEnvConfig.tenantKey,
1666
- client: {
1667
- clientId: propsConfig.clientId || apiEnvConfig.client.clientId,
1668
- redirectUri: apiEnvConfig.client.redirectUri,
1669
- scopes: apiEnvConfig.client.scopes
1670
- }
1671
- };
1672
- }
1673
- async function resolveTenantConfig(tenantKey, tenantResolver) {
1674
- if (!tenantKey) return null;
1675
- const propsConfig = getPropsConfig();
1676
- const resolved = await tenantResolver.resolve(tenantKey);
1677
- if (!resolved) return null;
1678
- removeStorage(getStorageKey("props_config"));
1679
- return {
1680
- ...propsConfig ? mergeConfigWithProps(resolved, propsConfig, tenantKey) : resolved,
1681
- tenantKey: tenantKey || resolved.tenantKey
1682
- };
1683
- }
1684
- async function resolveTenantConfigCached(tenantKey, tenantResolver) {
1685
- if (!tenantKey) return null;
1686
- return defaultTenantConfigCache.getOrResolve(
1687
- tenantKey,
1688
- (key) => resolveTenantConfig(key, tenantResolver)
1689
- );
1690
- }
1691
-
1692
- // src/application/flows/ForceLoginFlow.ts
1693
- var ForceLoginFlow = class {
1694
- async execute(context) {
1695
- const { runtime } = context;
1696
- const { urlProvider, logger: log } = runtime;
1697
- const getUrl = () => urlProvider.getCurrentUrl();
1698
- if (isCallbackUrlFromUrl(getUrl())) {
1699
- await handleCallback();
1700
- if (isCallbackUrlFromUrl(getUrl())) {
1701
- log.warn("Callback URL still present after cleanup, forcing URL clean");
1702
- const u = getUrl();
1703
- urlProvider.redirect(`${u.origin}${u.pathname}`);
1704
- return { completed: true };
1705
- }
1706
- }
1707
- clearFlowFlags();
1708
- const tenantKey = getTenantKeyFromRuntime(getUrl(), runtime.configProvider);
1709
- if (!tenantKey) return { completed: true };
1710
- const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
1711
- if (!tenantConfig) {
1712
- log.error("Tenant config resolution failed for force login", { tenantKey });
1713
- throw new SanitizedError(
1714
- "TENANT_CONFIG_RESOLUTION_FAILED",
1715
- "Unable to resolve tenant configuration",
1716
- new Error(`Tenant config resolution failed for tenantKey: ${tenantKey}`)
1717
- );
1371
+ cleanUrlSoft();
1372
+ resetRedirectLoopGuard();
1373
+ emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
1374
+ return true;
1718
1375
  }
1719
- storeTenantConfig(tenantConfig);
1720
- await initiateAuthFlow(tenantConfig);
1721
- return { completed: true };
1376
+ emitAuthEvent(AuthEventNames.CALLBACK_FAILED, {
1377
+ error: err instanceof Error ? err.message : String(err)
1378
+ });
1379
+ clearProcessing();
1380
+ if (isCallbackUrl()) cleanUrlSoft();
1381
+ return false;
1722
1382
  }
1723
- };
1383
+ }
1724
1384
 
1725
1385
  // src/application/flows/CallbackFlow.ts
1726
1386
  var CallbackFlow = class {
@@ -1791,6 +1451,206 @@ function getAccessToken2() {
1791
1451
  return getAccessToken();
1792
1452
  }
1793
1453
 
1454
+ // src/strategies/oauth2-pkce/pkce.ts
1455
+ function generateCodeVerifier(length = 128) {
1456
+ const array = new Uint8Array(length);
1457
+ crypto.getRandomValues(array);
1458
+ const base64 = btoa(String.fromCharCode(...array));
1459
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1460
+ }
1461
+ async function generateCodeChallenge(verifier) {
1462
+ try {
1463
+ const encoder = new TextEncoder();
1464
+ const data = encoder.encode(verifier);
1465
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1466
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
1467
+ const base64 = btoa(String.fromCharCode(...hashArray));
1468
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1469
+ } catch {
1470
+ throw new Error("Failed to generate code challenge");
1471
+ }
1472
+ }
1473
+ function generateState() {
1474
+ const array = new Uint8Array(32);
1475
+ crypto.getRandomValues(array);
1476
+ const base64 = btoa(String.fromCharCode(...array));
1477
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1478
+ }
1479
+
1480
+ // src/utils/validation.ts
1481
+ function isValidGuid(guid) {
1482
+ if (!guid || typeof guid !== "string") {
1483
+ return false;
1484
+ }
1485
+ const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1486
+ return guidRegex.test(guid.trim());
1487
+ }
1488
+ function validateTenantId(tenantId) {
1489
+ if (!tenantId || tenantId.trim() === "") {
1490
+ return {
1491
+ isValid: false,
1492
+ error: "Tenant ID is required"
1493
+ };
1494
+ }
1495
+ if (!isValidGuid(tenantId)) {
1496
+ return {
1497
+ isValid: false,
1498
+ error: "Tenant ID must be a valid GUID format (00000000-0000-0000-0000-000000000001)"
1499
+ };
1500
+ }
1501
+ return { isValid: true };
1502
+ }
1503
+
1504
+ // src/strategies/oauth2-pkce/oauth.ts
1505
+ function storeTenantConfig(config) {
1506
+ const validation = validateTenantId(config.tenantId);
1507
+ if (!validation.isValid) {
1508
+ logger.warn("Tenant ID validation failed", { tenantId: config.tenantId });
1509
+ }
1510
+ const authServerToStore = Config.getAuthServerUrl() || config.authServer;
1511
+ const redirectUriToStore = Config.getRedirectUri();
1512
+ const hostConfigKey = getStorageKey("host_config");
1513
+ let existing = {};
1514
+ try {
1515
+ const raw = getStorage(hostConfigKey);
1516
+ if (raw) existing = JSON.parse(raw);
1517
+ } catch {
1518
+ existing = {};
1519
+ }
1520
+ setStorage(
1521
+ hostConfigKey,
1522
+ JSON.stringify({
1523
+ ...existing,
1524
+ authServerUrl: authServerToStore,
1525
+ clientId: config.client.clientId,
1526
+ redirectUri: redirectUriToStore,
1527
+ tenantId: config.tenantId,
1528
+ tenantKey: config.tenantKey ?? existing.tenantKey ?? "",
1529
+ tenantCode: config.tenantKey ?? existing.tenantCode ?? ""
1530
+ })
1531
+ );
1532
+ if (config.tenantKey) {
1533
+ setStorage(STORAGE_KEYS.tenantKey, config.tenantKey);
1534
+ }
1535
+ }
1536
+ async function initiateAuthFlow(config) {
1537
+ try {
1538
+ logger.info("Initiating OAuth authorization flow");
1539
+ const authServerFromProps = Config.getAuthServerUrl();
1540
+ const authServerEffective = (authServerFromProps || config?.authServer || "").replace(/\/+$/, "");
1541
+ const redirectUriEffective = Config.getRedirectUri();
1542
+ const tenantIdEffective = config?.tenantId || "";
1543
+ const clientIdEffective = config?.client?.clientId || "";
1544
+ logger.debug("[initiateAuthFlow] Resolved inputs", {
1545
+ authServerFromProps: authServerFromProps ? "(provided)" : "(empty)",
1546
+ authServerEffective,
1547
+ redirectUriEffective,
1548
+ tenantIdEffective,
1549
+ clientIdEffective,
1550
+ scopes: config?.client?.scopes,
1551
+ hasRuntime: !!getAuthRuntime()
1552
+ });
1553
+ const token = getAccessToken();
1554
+ if (token && !isAccessTokenExpired()) {
1555
+ logger.debug("Valid token already exists, skipping auth flow");
1556
+ clearFlowFlags();
1557
+ return;
1558
+ }
1559
+ if (isCallbackUrl()) {
1560
+ logger.debug("Callback URL detected - clearing flow flag to allow callback processing");
1561
+ clearFlowFlags();
1562
+ return;
1563
+ }
1564
+ if (isFlowInProgress()) {
1565
+ if (isFlowFlagStale(3e3)) {
1566
+ logger.info("Stale auth flow flag detected, clearing and proceeding");
1567
+ clearFlowFlags();
1568
+ } else {
1569
+ logger.debug("Auth flow already in progress (recent), skipping duplicate redirect");
1570
+ return;
1571
+ }
1572
+ }
1573
+ setFlowFlags();
1574
+ if (!(Config.getAuthServerUrl() || config?.authServer) || !config.client?.clientId || !config.client?.redirectUri) {
1575
+ logger.error("[initiateAuthFlow] Missing required tenant config", {
1576
+ authServerEffective,
1577
+ clientId: config?.client?.clientId,
1578
+ redirectUriFromTenantApi: config?.client?.redirectUri,
1579
+ redirectUriEffective
1580
+ });
1581
+ throw new Error("Invalid tenant configuration for auth flow");
1582
+ }
1583
+ storeTenantConfig(config);
1584
+ logger.debug("[initiateAuthFlow] Stored tenant config, generating PKCE");
1585
+ removeStorage(getStorageKey("callback_processed"));
1586
+ removeStorage(getStorageKey("state_mismatch"));
1587
+ clearCallbackProcessing();
1588
+ const codeVerifier = generateCodeVerifier();
1589
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
1590
+ const state = generateState();
1591
+ removeStorage(STORAGE_KEYS.codeVerifier);
1592
+ removeStorage(STORAGE_KEYS.state);
1593
+ setStorage(STORAGE_KEYS.codeVerifier, codeVerifier);
1594
+ setStorage(STORAGE_KEYS.state, state);
1595
+ const authUrl = buildAuthorizationUrl(config, codeChallenge, state);
1596
+ logger.debug("[initiateAuthFlow] Built authorization URL", {
1597
+ authServerEffective,
1598
+ urlPrefix: authUrl?.slice(0, 80)
1599
+ });
1600
+ if (!authUrl || !(authUrl.startsWith("http://") || authUrl.startsWith("https://"))) {
1601
+ throw new Error("Invalid authorization URL generated");
1602
+ }
1603
+ if (!checkAndIncrementRedirect()) {
1604
+ logger.warn("Redirect loop protection: max redirect attempts exceeded");
1605
+ clearFlowFlags();
1606
+ return;
1607
+ }
1608
+ logger.info("Redirecting to authorization server");
1609
+ emitAuthEvent(AuthEventNames.LOGIN_STARTED, { tenantKey: config.tenantKey });
1610
+ const runtime = getAuthRuntime();
1611
+ if (runtime) {
1612
+ logger.debug("[initiateAuthFlow] Redirect via runtime.urlProvider.redirect");
1613
+ runtime.urlProvider.redirect(authUrl);
1614
+ } else if (typeof window !== "undefined") {
1615
+ logger.debug("[initiateAuthFlow] Redirect via window.location.replace");
1616
+ window.location.replace(authUrl);
1617
+ } else {
1618
+ logger.error("[initiateAuthFlow] No runtime and no window; cannot redirect", { authUrl });
1619
+ }
1620
+ } catch (error) {
1621
+ clearFlowFlags();
1622
+ logger.error("Failed to initiate auth flow", {
1623
+ error: error instanceof Error ? error.message : String(error)
1624
+ });
1625
+ throw error;
1626
+ }
1627
+ }
1628
+ function buildAuthorizationUrl(config, codeChallenge, state) {
1629
+ const authServerBase = (Config.getAuthServerUrl() || config.authServer || "").replace(/\/+$/, "");
1630
+ if (isLocalhost2()) {
1631
+ logger.debug("Building authorization URL", {
1632
+ authServer: authServerBase,
1633
+ clientId: config.client.clientId,
1634
+ redirectUri: config.client.redirectUri,
1635
+ scopes: config.client.scopes
1636
+ });
1637
+ }
1638
+ const tenantIdToUse = config.tenantId;
1639
+ const clientIdToUse = config.client.clientId;
1640
+ const redirectUriToUse = Config.getRedirectUri();
1641
+ const params = new URLSearchParams({
1642
+ client_id: `public-${clientIdToUse}`,
1643
+ tenant_id: tenantIdToUse,
1644
+ redirect_uri: redirectUriToUse,
1645
+ response_type: DEFAULT_RESPONSE_TYPE,
1646
+ scope: config.client.scopes,
1647
+ state,
1648
+ code_challenge: codeChallenge,
1649
+ code_challenge_method: DEFAULT_CODE_CHALLENGE_METHOD
1650
+ });
1651
+ return `${authServerBase}/connect/authorize?${params.toString()}`;
1652
+ }
1653
+
1794
1654
  // src/application/flows/RefreshFlow.ts
1795
1655
  var RefreshFlow = class {
1796
1656
  async execute(context) {
@@ -1809,12 +1669,16 @@ var RefreshFlow = class {
1809
1669
  error: error instanceof Error ? error.message : String(error)
1810
1670
  });
1811
1671
  }
1812
- const tenantKey = getTenantKeyFromRuntime(getUrl(), runtime.configProvider);
1672
+ const tenantKey = resolveTenantKey();
1673
+ log.debug("[RefreshFlow] Tenant key resolved", {
1674
+ tenantKey: tenantKey ?? "(null)",
1675
+ source: tenantKey ? "resolved" : "none"
1676
+ });
1813
1677
  if (!tenantKey) {
1814
1678
  throw new SanitizedError(
1815
1679
  "TENANT_KEY_RESOLUTION_FAILED",
1816
1680
  "Unable to resolve tenant",
1817
- new Error("Tenant key missing")
1681
+ new Error(`Tenant key missing for refresh flow at url=${getUrl().href}`)
1818
1682
  );
1819
1683
  }
1820
1684
  const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
@@ -1843,12 +1707,17 @@ var TenantSwitchFlow = class {
1843
1707
  emitAuthEvent(AuthEventNames.TENANT_SWITCHED, { tenantKey: newTenantKey ?? void 0 });
1844
1708
  removeStorage(getStorageKey("tenant_switched"));
1845
1709
  removeStorage(getStorageKey("new_tenant_key"));
1846
- const tenantKey = newTenantKey || getTenantKeyFromRuntime(getUrl(), runtime.configProvider);
1710
+ const tenantKey = (newTenantKey || resolveTenantKey())?.trim() || null;
1711
+ log.debug("[TenantSwitchFlow] Tenant key resolved", {
1712
+ tenantKey: tenantKey ?? "(null)",
1713
+ fromNewTenantKey: newTenantKey ?? "(null)",
1714
+ source: newTenantKey ? "new_tenant_key" : tenantKey ? "resolved" : "none"
1715
+ });
1847
1716
  if (!tenantKey) {
1848
1717
  throw new SanitizedError(
1849
1718
  "TENANT_KEY_RESOLUTION_FAILED",
1850
1719
  "Unable to resolve tenant after switch",
1851
- new Error("Tenant key missing after switch")
1720
+ new Error(`Tenant key missing after switch at url=${getUrl().href}`)
1852
1721
  );
1853
1722
  }
1854
1723
  const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
@@ -1859,6 +1728,16 @@ var TenantSwitchFlow = class {
1859
1728
  new Error(`Tenant config resolution failed for tenantKey: ${tenantKey}`)
1860
1729
  );
1861
1730
  }
1731
+ try {
1732
+ const cfg = getAuthConfig();
1733
+ setStorage(getStorageKey("host_config"), JSON.stringify({
1734
+ ...cfg,
1735
+ tenantKey,
1736
+ tenantCode: tenantKey,
1737
+ clientCode: tenantConfig.client?.clientCode ?? cfg.clientCode ?? ""
1738
+ }));
1739
+ } catch {
1740
+ }
1862
1741
  setTenantKey(tenantKey);
1863
1742
  storeTenantConfig(tenantConfig);
1864
1743
  await initiateAuthFlow(tenantConfig);
@@ -1883,13 +1762,16 @@ var LoginFlow = class {
1883
1762
  }
1884
1763
  const wasStateMismatch = getStorage(getStorageKey("state_mismatch")) === "true";
1885
1764
  if (wasStateMismatch) removeStorage(getStorageKey("state_mismatch"));
1886
- const tenantKey = getTenantKeyFromRuntime(url, runtime.configProvider);
1887
- log.debug("[LoginFlow] Tenant key resolved", { tenantKey: tenantKey ?? "(null)" });
1765
+ const tenantKey = resolveTenantKey();
1766
+ log.debug("[LoginFlow] Tenant key resolved", {
1767
+ tenantKey: tenantKey ?? "(null)",
1768
+ source: tenantKey ? "resolved" : "none"
1769
+ });
1888
1770
  if (!tenantKey) {
1889
1771
  throw new SanitizedError(
1890
1772
  "TENANT_KEY_RESOLUTION_FAILED",
1891
1773
  "Unable to resolve tenant",
1892
- new Error("Tenant key missing")
1774
+ new Error(`Tenant key missing for login flow at url=${url.href}`)
1893
1775
  );
1894
1776
  }
1895
1777
  log.debug("[LoginFlow] Resolving tenant config", { tenantKey });
@@ -1918,7 +1800,6 @@ var LoginFlow = class {
1918
1800
  };
1919
1801
 
1920
1802
  // src/application/flows/getFlowForState.ts
1921
- var forceLoginFlow = new ForceLoginFlow();
1922
1803
  var callbackFlow = new CallbackFlow();
1923
1804
  var authenticatedFlow = new AuthenticatedFlow();
1924
1805
  var refreshFlow = new RefreshFlow();
@@ -1926,8 +1807,6 @@ var tenantSwitchFlow = new TenantSwitchFlow();
1926
1807
  var loginFlow = new LoginFlow();
1927
1808
  function getFlowForState(state) {
1928
1809
  switch (state) {
1929
- case "FORCE_LOGIN" /* FORCE_LOGIN */:
1930
- return forceLoginFlow;
1931
1810
  case "PROCESSING_CALLBACK" /* PROCESSING_CALLBACK */:
1932
1811
  return callbackFlow;
1933
1812
  case "AUTHENTICATED" /* AUTHENTICATED */:
@@ -2148,34 +2027,8 @@ function checkCookiesDeleted() {
2148
2027
  function handleCookieDeletion() {
2149
2028
  try {
2150
2029
  logger.info("Cookies deleted detected - clearing session and redirecting to login");
2151
- const forceLoginSet = getStorage(getStorageKey("force_login")) === "true";
2152
- const logoutInProgress = getStorage(getStorageKey("logout_in_progress")) === "true";
2153
- if (forceLoginSet || logoutInProgress) {
2154
- logger.debug(
2155
- "Force login or logout in progress - skipping bootstrap from cookie watcher (avoids duplicate by-code API call)"
2156
- );
2157
- return;
2158
- }
2159
2030
  clearAuthStorage();
2160
- try {
2161
- if (typeof window !== "undefined") {
2162
- const forceLogin = window.localStorage.getItem(getStorageKey("force_login"));
2163
- const logoutInProgressPreserve = window.localStorage.getItem(
2164
- getStorageKey("logout_in_progress")
2165
- );
2166
- clearAuthSessionStorage();
2167
- if (forceLogin === "true")
2168
- window.localStorage.setItem(getStorageKey("force_login"), "true");
2169
- if (logoutInProgressPreserve === "true")
2170
- window.localStorage.setItem(getStorageKey("logout_in_progress"), "true");
2171
- } else {
2172
- clearAuthSessionStorage();
2173
- }
2174
- } catch (error) {
2175
- logger.warn("Failed to clear sessionStorage", {
2176
- error: error instanceof Error ? error.message : String(error)
2177
- });
2178
- }
2031
+ clearAuthSessionStorage();
2179
2032
  const cb = getSessionClearedCallback();
2180
2033
  if (cb) {
2181
2034
  Promise.resolve(cb()).catch((error) => {
@@ -2209,13 +2062,6 @@ function startCookieWatcher() {
2209
2062
  allCookies: initialCookies
2210
2063
  });
2211
2064
  watchInterval = setInterval(async () => {
2212
- const logoutInProgress = typeof window !== "undefined" && window.localStorage.getItem(getStorageKey("logout_in_progress")) === "true";
2213
- if (logoutInProgress) {
2214
- logger.debug("Logout detected. Cookie watcher will not trigger bootstrap.");
2215
- lastCookieSnapshot = getCookieSnapshot();
2216
- lastCookieMap = getCookieMap();
2217
- return;
2218
- }
2219
2065
  const cookiesDeleted = checkCookiesDeleted();
2220
2066
  if (cookiesDeleted) {
2221
2067
  logger.info("Cookies deleted detected by cookie watcher");
@@ -2301,11 +2147,6 @@ function startStorageWatcher() {
2301
2147
  storageEventListener = handleStorageEvent;
2302
2148
  window.addEventListener("storage", storageEventListener);
2303
2149
  const pollInterval = setInterval(() => {
2304
- const logoutInProgress = typeof window !== "undefined" && window.localStorage.getItem(getStorageKey("logout_in_progress")) === "true";
2305
- if (logoutInProgress) {
2306
- logger.debug("Logout detected. Storage watcher will not trigger bootstrap.");
2307
- return;
2308
- }
2309
2150
  const wasCleared = checkStorageCleared();
2310
2151
  if (wasCleared) {
2311
2152
  logger.info("Detected manual sessionStorage clear (polling)! Redirecting to login...");
@@ -2378,6 +2219,27 @@ function buildUrl(baseUrl, path) {
2378
2219
  url.pathname = url.pathname.endsWith("/") ? `${url.pathname}${normalizedPath.slice(1)}` : `${url.pathname}${normalizedPath}`;
2379
2220
  return url.toString();
2380
2221
  }
2222
+ function snapshotHostConfig() {
2223
+ try {
2224
+ const raw = getStorage(getStorageKey("host_config"));
2225
+ if (!raw) return null;
2226
+ const parsed = JSON.parse(raw);
2227
+ return parsed && typeof parsed === "object" ? parsed : null;
2228
+ } catch {
2229
+ return null;
2230
+ }
2231
+ }
2232
+ function restoreHostConfig(config) {
2233
+ if (!config) return;
2234
+ try {
2235
+ setStorage(getStorageKey("host_config"), JSON.stringify(config));
2236
+ logger.debug("Restored host_config after logout cleanup");
2237
+ } catch (error) {
2238
+ logger.warn("Failed to restore host_config after logout cleanup", {
2239
+ error: error instanceof Error ? error.message : String(error)
2240
+ });
2241
+ }
2242
+ }
2381
2243
  function getCsrfToken() {
2382
2244
  try {
2383
2245
  if (typeof document === "undefined") return null;
@@ -2485,9 +2347,14 @@ function releaseLogoutLock() {
2485
2347
  }
2486
2348
  }
2487
2349
  async function logout(tenantKey) {
2488
- setStorage(getStorageKey("force_login"), "true");
2350
+ const preservedHostConfig = snapshotHostConfig();
2489
2351
  if (tenantKey?.trim()) {
2490
- setStorage(getStorageKey("preserved_tenant_key"), tenantKey.trim());
2352
+ const updated = {
2353
+ ...preservedHostConfig ?? {},
2354
+ tenantKey: tenantKey.trim(),
2355
+ tenantCode: tenantKey.trim()
2356
+ };
2357
+ setStorage(getStorageKey("host_config"), JSON.stringify(updated));
2491
2358
  }
2492
2359
  if (!acquireLogoutLock()) {
2493
2360
  return { success: true, message: "Logout already in progress." };
@@ -2495,7 +2362,8 @@ async function logout(tenantKey) {
2495
2362
  try {
2496
2363
  stopStorageWatcher();
2497
2364
  stopCookieWatcher();
2498
- const apiAuthBaseUrl = Config.getAuthServerUrl();
2365
+ const authConfig = getAuthConfig();
2366
+ const apiAuthBaseUrl = authConfig.authServerUrl || Config.getAuthServerUrl();
2499
2367
  let serverLogoutSuccess = false;
2500
2368
  let serverResponse;
2501
2369
  if (apiAuthBaseUrl) {
@@ -2514,25 +2382,7 @@ async function logout(tenantKey) {
2514
2382
  logger.warn("API auth base URL not configured, performing local logout only");
2515
2383
  }
2516
2384
  performLocalLogoutCleanup();
2517
- const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
2518
- if (preservedTenantKey) {
2519
- const propsKey = getStorageKey("props_config");
2520
- let existing = {};
2521
- try {
2522
- const raw = getStorage(propsKey);
2523
- if (raw) {
2524
- existing = JSON.parse(raw);
2525
- }
2526
- } catch {
2527
- existing = {};
2528
- }
2529
- const merged = {
2530
- ...existing,
2531
- tenantKey: preservedTenantKey
2532
- };
2533
- setStorage(propsKey, JSON.stringify(merged));
2534
- removeStorage(getStorageKey("preserved_tenant_key"));
2535
- }
2385
+ restoreHostConfig(preservedHostConfig);
2536
2386
  resetCookieWatcher();
2537
2387
  startStorageWatcher();
2538
2388
  releaseLogoutLock();
@@ -2542,7 +2392,7 @@ async function logout(tenantKey) {
2542
2392
  });
2543
2393
  emitAuthEvent(AuthEventNames.LOGOUT_COMPLETED, {
2544
2394
  serverLogoutSuccess,
2545
- tenantKey: getStorage(getStorageKey("preserved_tenant_key")) || void 0,
2395
+ tenantKey: tenantKey?.trim() || preservedHostConfig?.tenantKey || void 0,
2546
2396
  message: serverLogoutSuccess ? "Server logout success" : "Local logout only"
2547
2397
  });
2548
2398
  return {
@@ -2555,25 +2405,7 @@ async function logout(tenantKey) {
2555
2405
  error: error instanceof Error ? error.message : String(error)
2556
2406
  });
2557
2407
  performLocalLogoutCleanup();
2558
- const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
2559
- if (preservedTenantKey) {
2560
- const propsKey = getStorageKey("props_config");
2561
- let existing = {};
2562
- try {
2563
- const raw = getStorage(propsKey);
2564
- if (raw) {
2565
- existing = JSON.parse(raw);
2566
- }
2567
- } catch {
2568
- existing = {};
2569
- }
2570
- const merged = {
2571
- ...existing,
2572
- tenantKey: preservedTenantKey
2573
- };
2574
- setStorage(propsKey, JSON.stringify(merged));
2575
- removeStorage(getStorageKey("preserved_tenant_key"));
2576
- }
2408
+ restoreHostConfig(preservedHostConfig);
2577
2409
  releaseLogoutLock();
2578
2410
  return {
2579
2411
  success: true,
@@ -2926,13 +2758,19 @@ function AuthProvider({
2926
2758
  }, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
2927
2759
  react.useEffect(() => {
2928
2760
  try {
2761
+ const hostConfigKey = getStorageKey("host_config");
2762
+ const raw = getStorage(hostConfigKey);
2763
+ const existing = raw ? JSON.parse(raw) : {};
2929
2764
  const payload = {
2765
+ ...existing,
2930
2766
  tenantCode: mergedTenantCode ?? "",
2767
+ tenantKey: mergedTenantCode ?? "",
2768
+ appId: appId ?? "",
2931
2769
  apiBaseUrl: mergedApiBaseUrl ?? "",
2932
2770
  authServerUrl: mergedAuthServerUrl ?? "",
2933
2771
  callbackUrl: mergedCallbackUrl ?? ""
2934
2772
  };
2935
- setStorage(getStorageKey("host_config"), JSON.stringify(payload));
2773
+ setStorage(hostConfigKey, JSON.stringify(payload));
2936
2774
  } catch {
2937
2775
  }
2938
2776
  }, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
@@ -2976,24 +2814,18 @@ function AuthProvider({
2976
2814
  const currentUrl = window.location.href;
2977
2815
  const isCallback = currentUrl.includes("code=") || currentUrl.includes("state=");
2978
2816
  if (isCallback) {
2979
- const cleanUrl2 = window.location.origin + window.location.pathname;
2980
- window.history.replaceState({}, document.title, cleanUrl2);
2817
+ const cleanUrl = window.location.origin + window.location.pathname;
2818
+ window.history.replaceState({}, document.title, cleanUrl);
2981
2819
  }
2982
2820
  }
2983
2821
  removeStorage(getStorageKey("access_token"));
2984
2822
  removeStorage(getStorageKey("refresh_token"));
2985
2823
  removeStorage(getStorageKey("expires_at"));
2986
- removeStorage(getStorageKey("id_token"));
2987
2824
  removeStorage(getStorageKey("state"));
2988
2825
  removeStorage(getStorageKey("code_verifier"));
2989
2826
  removeStorage(getStorageKey("callback_processed"));
2990
2827
  clearCallbackProcessing();
2991
2828
  clearFlowFlags();
2992
- removeStorage(getStorageKey("tenant_id"));
2993
- if (typeof window !== "undefined") {
2994
- window.localStorage.removeItem(getStorageKey("force_login"));
2995
- window.localStorage.removeItem(getStorageKey("logout_in_progress"));
2996
- }
2997
2829
  setStorage(getStorageKey("props_changed"), "true");
2998
2830
  setStorage(getStorageKey("tenant_switched"), "true");
2999
2831
  setStorage(getStorageKey("new_tenant_key"), resolvedTenantKey2);
@@ -3006,7 +2838,17 @@ function AuthProvider({
3006
2838
  } else if (previousTenantKey) {
3007
2839
  logger.debug("[AuthProvider] TenantKey unchanged", { tenantKey: resolvedTenantKey2 });
3008
2840
  }
3009
- setStorage(getStorageKey("props_config"), JSON.stringify({ tenantKey: resolvedTenantKey2 }));
2841
+ try {
2842
+ const hostConfigKey = getStorageKey("host_config");
2843
+ const raw = getStorage(hostConfigKey);
2844
+ const existing = raw ? JSON.parse(raw) : {};
2845
+ setStorage(hostConfigKey, JSON.stringify({
2846
+ ...existing,
2847
+ tenantKey: resolvedTenantKey2,
2848
+ tenantCode: resolvedTenantKey2
2849
+ }));
2850
+ } catch {
2851
+ }
3010
2852
  setStorage(getStorageKey("tenant_key"), resolvedTenantKey2);
3011
2853
  } else {
3012
2854
  logger.debug("[AuthProvider] No tenant code determined");
@@ -3205,17 +3047,29 @@ function AuthCallback({
3205
3047
  setStatus("loading");
3206
3048
  setMessage("Completing sign-in...");
3207
3049
  const handled = await handleCallback();
3208
- if (handled) {
3209
- const token = getAccessToken();
3210
- if (token) setTokens(token);
3211
- setStatus("success");
3212
- setMessage("Authentication successful. Redirecting...");
3213
- window.location.replace(successPath);
3214
- } else {
3050
+ if (!handled) {
3215
3051
  setStatus("error");
3216
3052
  setMessage("Authentication failed. Please try again.");
3217
3053
  window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
3054
+ return;
3055
+ }
3056
+ const startedAt = Date.now();
3057
+ const MAX_WAIT_MS = 5e3;
3058
+ while (true) {
3059
+ const token = getAccessToken();
3060
+ if (token) {
3061
+ setTokens(token);
3062
+ setStatus("success");
3063
+ setMessage("Authentication successful. Redirecting...");
3064
+ window.location.replace(successPath);
3065
+ return;
3066
+ }
3067
+ if (Date.now() - startedAt > MAX_WAIT_MS) break;
3068
+ await new Promise((r) => setTimeout(r, 200));
3218
3069
  }
3070
+ setStatus("error");
3071
+ setMessage("Authentication did not complete. Please try again.");
3072
+ window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
3219
3073
  };
3220
3074
  runCallback();
3221
3075
  }, [homePath, loginPath, successPath, setTokens]);
@@ -3417,6 +3271,7 @@ exports.enforceHttps = enforceHttps;
3417
3271
  exports.ensureValidAccessToken = ensureValidAccessToken;
3418
3272
  exports.extractTenantKey = extractTenantKey;
3419
3273
  exports.getAccessToken = getAccessToken2;
3274
+ exports.getAuthConfig = getAuthConfig;
3420
3275
  exports.getAuthRuntime = getAuthRuntime;
3421
3276
  exports.getConfig = getConfig;
3422
3277
  exports.getStorageAdapter = getStorageAdapter;