tenneo-auth-plugin 1.0.0 → 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);
@@ -1554,173 +1365,22 @@ async function handleCallback() {
1554
1365
  }
1555
1366
  clearAllAuthFlags();
1556
1367
  const existingToken = getAccessToken();
1557
- const tokenValid = existingToken && !isAccessTokenExpired();
1558
- if (tokenValid) {
1559
- 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
- );
1368
+ const tokenValid = existingToken && !isAccessTokenExpired();
1369
+ if (tokenValid) {
1370
+ markProcessed();
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,21 +1669,16 @@ var RefreshFlow = class {
1809
1669
  error: error instanceof Error ? error.message : String(error)
1810
1670
  });
1811
1671
  }
1812
- const tenantKeyFromRuntime = getTenantKeyFromRuntime(getUrl(), runtime.configProvider);
1813
- const tenantKeyFromStorage = getStorage(getStorageKey("tenant_key"));
1814
- const tenantKeyFromConfig = runtime.configProvider.getClientCode?.() ?? "";
1815
- const tenantKey = (tenantKeyFromRuntime || tenantKeyFromStorage || tenantKeyFromConfig)?.trim();
1672
+ const tenantKey = resolveTenantKey();
1816
1673
  log.debug("[RefreshFlow] Tenant key resolved", {
1817
1674
  tenantKey: tenantKey ?? "(null)",
1818
- fromRuntime: tenantKeyFromRuntime ?? "(null)",
1819
- fromStorage: tenantKeyFromStorage ?? "(null)",
1820
- fromConfig: tenantKeyFromConfig || "(null)"
1675
+ source: tenantKey ? "resolved" : "none"
1821
1676
  });
1822
1677
  if (!tenantKey) {
1823
1678
  throw new SanitizedError(
1824
1679
  "TENANT_KEY_RESOLUTION_FAILED",
1825
1680
  "Unable to resolve tenant",
1826
- new Error("Tenant key missing")
1681
+ new Error(`Tenant key missing for refresh flow at url=${getUrl().href}`)
1827
1682
  );
1828
1683
  }
1829
1684
  const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
@@ -1852,22 +1707,17 @@ var TenantSwitchFlow = class {
1852
1707
  emitAuthEvent(AuthEventNames.TENANT_SWITCHED, { tenantKey: newTenantKey ?? void 0 });
1853
1708
  removeStorage(getStorageKey("tenant_switched"));
1854
1709
  removeStorage(getStorageKey("new_tenant_key"));
1855
- const tenantKeyFromRuntime = getTenantKeyFromRuntime(getUrl(), runtime.configProvider);
1856
- const tenantKeyFromStorage = getStorage(getStorageKey("tenant_key"));
1857
- const tenantKeyFromConfig = runtime.configProvider.getClientCode?.() ?? "";
1858
- const tenantKey = (newTenantKey || tenantKeyFromRuntime || tenantKeyFromStorage || tenantKeyFromConfig)?.trim();
1710
+ const tenantKey = (newTenantKey || resolveTenantKey())?.trim() || null;
1859
1711
  log.debug("[TenantSwitchFlow] Tenant key resolved", {
1860
1712
  tenantKey: tenantKey ?? "(null)",
1861
1713
  fromNewTenantKey: newTenantKey ?? "(null)",
1862
- fromRuntime: tenantKeyFromRuntime ?? "(null)",
1863
- fromStorage: tenantKeyFromStorage ?? "(null)",
1864
- fromConfig: tenantKeyFromConfig || "(null)"
1714
+ source: newTenantKey ? "new_tenant_key" : tenantKey ? "resolved" : "none"
1865
1715
  });
1866
1716
  if (!tenantKey) {
1867
1717
  throw new SanitizedError(
1868
1718
  "TENANT_KEY_RESOLUTION_FAILED",
1869
1719
  "Unable to resolve tenant after switch",
1870
- new Error("Tenant key missing after switch")
1720
+ new Error(`Tenant key missing after switch at url=${getUrl().href}`)
1871
1721
  );
1872
1722
  }
1873
1723
  const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
@@ -1878,6 +1728,16 @@ var TenantSwitchFlow = class {
1878
1728
  new Error(`Tenant config resolution failed for tenantKey: ${tenantKey}`)
1879
1729
  );
1880
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
+ }
1881
1741
  setTenantKey(tenantKey);
1882
1742
  storeTenantConfig(tenantConfig);
1883
1743
  await initiateAuthFlow(tenantConfig);
@@ -1902,21 +1762,16 @@ var LoginFlow = class {
1902
1762
  }
1903
1763
  const wasStateMismatch = getStorage(getStorageKey("state_mismatch")) === "true";
1904
1764
  if (wasStateMismatch) removeStorage(getStorageKey("state_mismatch"));
1905
- const tenantKeyFromRuntime = getTenantKeyFromRuntime(url, runtime.configProvider);
1906
- const tenantKeyFromStorage = getStorage(getStorageKey("tenant_key"));
1907
- const tenantKeyFromConfig = runtime.configProvider.getClientCode?.() ?? "";
1908
- const tenantKey = (tenantKeyFromRuntime || tenantKeyFromStorage || tenantKeyFromConfig)?.trim();
1765
+ const tenantKey = resolveTenantKey();
1909
1766
  log.debug("[LoginFlow] Tenant key resolved", {
1910
1767
  tenantKey: tenantKey ?? "(null)",
1911
- fromRuntime: tenantKeyFromRuntime ?? "(null)",
1912
- fromStorage: tenantKeyFromStorage ?? "(null)",
1913
- fromConfig: tenantKeyFromConfig || "(null)"
1768
+ source: tenantKey ? "resolved" : "none"
1914
1769
  });
1915
1770
  if (!tenantKey) {
1916
1771
  throw new SanitizedError(
1917
1772
  "TENANT_KEY_RESOLUTION_FAILED",
1918
1773
  "Unable to resolve tenant",
1919
- new Error("Tenant key missing")
1774
+ new Error(`Tenant key missing for login flow at url=${url.href}`)
1920
1775
  );
1921
1776
  }
1922
1777
  log.debug("[LoginFlow] Resolving tenant config", { tenantKey });
@@ -1945,7 +1800,6 @@ var LoginFlow = class {
1945
1800
  };
1946
1801
 
1947
1802
  // src/application/flows/getFlowForState.ts
1948
- var forceLoginFlow = new ForceLoginFlow();
1949
1803
  var callbackFlow = new CallbackFlow();
1950
1804
  var authenticatedFlow = new AuthenticatedFlow();
1951
1805
  var refreshFlow = new RefreshFlow();
@@ -1953,8 +1807,6 @@ var tenantSwitchFlow = new TenantSwitchFlow();
1953
1807
  var loginFlow = new LoginFlow();
1954
1808
  function getFlowForState(state) {
1955
1809
  switch (state) {
1956
- case "FORCE_LOGIN" /* FORCE_LOGIN */:
1957
- return forceLoginFlow;
1958
1810
  case "PROCESSING_CALLBACK" /* PROCESSING_CALLBACK */:
1959
1811
  return callbackFlow;
1960
1812
  case "AUTHENTICATED" /* AUTHENTICATED */:
@@ -2175,34 +2027,8 @@ function checkCookiesDeleted() {
2175
2027
  function handleCookieDeletion() {
2176
2028
  try {
2177
2029
  logger.info("Cookies deleted detected - clearing session and redirecting to login");
2178
- const forceLoginSet = getStorage(getStorageKey("force_login")) === "true";
2179
- const logoutInProgress = getStorage(getStorageKey("logout_in_progress")) === "true";
2180
- if (forceLoginSet || logoutInProgress) {
2181
- logger.debug(
2182
- "Force login or logout in progress - skipping bootstrap from cookie watcher (avoids duplicate by-code API call)"
2183
- );
2184
- return;
2185
- }
2186
2030
  clearAuthStorage();
2187
- try {
2188
- if (typeof window !== "undefined") {
2189
- const forceLogin = window.localStorage.getItem(getStorageKey("force_login"));
2190
- const logoutInProgressPreserve = window.localStorage.getItem(
2191
- getStorageKey("logout_in_progress")
2192
- );
2193
- clearAuthSessionStorage();
2194
- if (forceLogin === "true")
2195
- window.localStorage.setItem(getStorageKey("force_login"), "true");
2196
- if (logoutInProgressPreserve === "true")
2197
- window.localStorage.setItem(getStorageKey("logout_in_progress"), "true");
2198
- } else {
2199
- clearAuthSessionStorage();
2200
- }
2201
- } catch (error) {
2202
- logger.warn("Failed to clear sessionStorage", {
2203
- error: error instanceof Error ? error.message : String(error)
2204
- });
2205
- }
2031
+ clearAuthSessionStorage();
2206
2032
  const cb = getSessionClearedCallback();
2207
2033
  if (cb) {
2208
2034
  Promise.resolve(cb()).catch((error) => {
@@ -2236,13 +2062,6 @@ function startCookieWatcher() {
2236
2062
  allCookies: initialCookies
2237
2063
  });
2238
2064
  watchInterval = setInterval(async () => {
2239
- const logoutInProgress = typeof window !== "undefined" && window.localStorage.getItem(getStorageKey("logout_in_progress")) === "true";
2240
- if (logoutInProgress) {
2241
- logger.debug("Logout detected. Cookie watcher will not trigger bootstrap.");
2242
- lastCookieSnapshot = getCookieSnapshot();
2243
- lastCookieMap = getCookieMap();
2244
- return;
2245
- }
2246
2065
  const cookiesDeleted = checkCookiesDeleted();
2247
2066
  if (cookiesDeleted) {
2248
2067
  logger.info("Cookies deleted detected by cookie watcher");
@@ -2328,11 +2147,6 @@ function startStorageWatcher() {
2328
2147
  storageEventListener = handleStorageEvent;
2329
2148
  window.addEventListener("storage", storageEventListener);
2330
2149
  const pollInterval = setInterval(() => {
2331
- const logoutInProgress = typeof window !== "undefined" && window.localStorage.getItem(getStorageKey("logout_in_progress")) === "true";
2332
- if (logoutInProgress) {
2333
- logger.debug("Logout detected. Storage watcher will not trigger bootstrap.");
2334
- return;
2335
- }
2336
2150
  const wasCleared = checkStorageCleared();
2337
2151
  if (wasCleared) {
2338
2152
  logger.info("Detected manual sessionStorage clear (polling)! Redirecting to login...");
@@ -2405,6 +2219,27 @@ function buildUrl(baseUrl, path) {
2405
2219
  url.pathname = url.pathname.endsWith("/") ? `${url.pathname}${normalizedPath.slice(1)}` : `${url.pathname}${normalizedPath}`;
2406
2220
  return url.toString();
2407
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
+ }
2408
2243
  function getCsrfToken() {
2409
2244
  try {
2410
2245
  if (typeof document === "undefined") return null;
@@ -2512,9 +2347,14 @@ function releaseLogoutLock() {
2512
2347
  }
2513
2348
  }
2514
2349
  async function logout(tenantKey) {
2515
- setStorage(getStorageKey("force_login"), "true");
2350
+ const preservedHostConfig = snapshotHostConfig();
2516
2351
  if (tenantKey?.trim()) {
2517
- 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));
2518
2358
  }
2519
2359
  if (!acquireLogoutLock()) {
2520
2360
  return { success: true, message: "Logout already in progress." };
@@ -2522,7 +2362,8 @@ async function logout(tenantKey) {
2522
2362
  try {
2523
2363
  stopStorageWatcher();
2524
2364
  stopCookieWatcher();
2525
- const apiAuthBaseUrl = Config.getAuthServerUrl();
2365
+ const authConfig = getAuthConfig();
2366
+ const apiAuthBaseUrl = authConfig.authServerUrl || Config.getAuthServerUrl();
2526
2367
  let serverLogoutSuccess = false;
2527
2368
  let serverResponse;
2528
2369
  if (apiAuthBaseUrl) {
@@ -2541,25 +2382,7 @@ async function logout(tenantKey) {
2541
2382
  logger.warn("API auth base URL not configured, performing local logout only");
2542
2383
  }
2543
2384
  performLocalLogoutCleanup();
2544
- const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
2545
- if (preservedTenantKey) {
2546
- const propsKey = getStorageKey("props_config");
2547
- let existing = {};
2548
- try {
2549
- const raw = getStorage(propsKey);
2550
- if (raw) {
2551
- existing = JSON.parse(raw);
2552
- }
2553
- } catch {
2554
- existing = {};
2555
- }
2556
- const merged = {
2557
- ...existing,
2558
- tenantKey: preservedTenantKey
2559
- };
2560
- setStorage(propsKey, JSON.stringify(merged));
2561
- removeStorage(getStorageKey("preserved_tenant_key"));
2562
- }
2385
+ restoreHostConfig(preservedHostConfig);
2563
2386
  resetCookieWatcher();
2564
2387
  startStorageWatcher();
2565
2388
  releaseLogoutLock();
@@ -2569,7 +2392,7 @@ async function logout(tenantKey) {
2569
2392
  });
2570
2393
  emitAuthEvent(AuthEventNames.LOGOUT_COMPLETED, {
2571
2394
  serverLogoutSuccess,
2572
- tenantKey: getStorage(getStorageKey("preserved_tenant_key")) || void 0,
2395
+ tenantKey: tenantKey?.trim() || preservedHostConfig?.tenantKey || void 0,
2573
2396
  message: serverLogoutSuccess ? "Server logout success" : "Local logout only"
2574
2397
  });
2575
2398
  return {
@@ -2582,25 +2405,7 @@ async function logout(tenantKey) {
2582
2405
  error: error instanceof Error ? error.message : String(error)
2583
2406
  });
2584
2407
  performLocalLogoutCleanup();
2585
- const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
2586
- if (preservedTenantKey) {
2587
- const propsKey = getStorageKey("props_config");
2588
- let existing = {};
2589
- try {
2590
- const raw = getStorage(propsKey);
2591
- if (raw) {
2592
- existing = JSON.parse(raw);
2593
- }
2594
- } catch {
2595
- existing = {};
2596
- }
2597
- const merged = {
2598
- ...existing,
2599
- tenantKey: preservedTenantKey
2600
- };
2601
- setStorage(propsKey, JSON.stringify(merged));
2602
- removeStorage(getStorageKey("preserved_tenant_key"));
2603
- }
2408
+ restoreHostConfig(preservedHostConfig);
2604
2409
  releaseLogoutLock();
2605
2410
  return {
2606
2411
  success: true,
@@ -2953,13 +2758,19 @@ function AuthProvider({
2953
2758
  }, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
2954
2759
  react.useEffect(() => {
2955
2760
  try {
2761
+ const hostConfigKey = getStorageKey("host_config");
2762
+ const raw = getStorage(hostConfigKey);
2763
+ const existing = raw ? JSON.parse(raw) : {};
2956
2764
  const payload = {
2765
+ ...existing,
2957
2766
  tenantCode: mergedTenantCode ?? "",
2767
+ tenantKey: mergedTenantCode ?? "",
2768
+ appId: appId ?? "",
2958
2769
  apiBaseUrl: mergedApiBaseUrl ?? "",
2959
2770
  authServerUrl: mergedAuthServerUrl ?? "",
2960
2771
  callbackUrl: mergedCallbackUrl ?? ""
2961
2772
  };
2962
- setStorage(getStorageKey("host_config"), JSON.stringify(payload));
2773
+ setStorage(hostConfigKey, JSON.stringify(payload));
2963
2774
  } catch {
2964
2775
  }
2965
2776
  }, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
@@ -3003,24 +2814,18 @@ function AuthProvider({
3003
2814
  const currentUrl = window.location.href;
3004
2815
  const isCallback = currentUrl.includes("code=") || currentUrl.includes("state=");
3005
2816
  if (isCallback) {
3006
- const cleanUrl2 = window.location.origin + window.location.pathname;
3007
- window.history.replaceState({}, document.title, cleanUrl2);
2817
+ const cleanUrl = window.location.origin + window.location.pathname;
2818
+ window.history.replaceState({}, document.title, cleanUrl);
3008
2819
  }
3009
2820
  }
3010
2821
  removeStorage(getStorageKey("access_token"));
3011
2822
  removeStorage(getStorageKey("refresh_token"));
3012
2823
  removeStorage(getStorageKey("expires_at"));
3013
- removeStorage(getStorageKey("id_token"));
3014
2824
  removeStorage(getStorageKey("state"));
3015
2825
  removeStorage(getStorageKey("code_verifier"));
3016
2826
  removeStorage(getStorageKey("callback_processed"));
3017
2827
  clearCallbackProcessing();
3018
2828
  clearFlowFlags();
3019
- removeStorage(getStorageKey("tenant_id"));
3020
- if (typeof window !== "undefined") {
3021
- window.localStorage.removeItem(getStorageKey("force_login"));
3022
- window.localStorage.removeItem(getStorageKey("logout_in_progress"));
3023
- }
3024
2829
  setStorage(getStorageKey("props_changed"), "true");
3025
2830
  setStorage(getStorageKey("tenant_switched"), "true");
3026
2831
  setStorage(getStorageKey("new_tenant_key"), resolvedTenantKey2);
@@ -3033,7 +2838,17 @@ function AuthProvider({
3033
2838
  } else if (previousTenantKey) {
3034
2839
  logger.debug("[AuthProvider] TenantKey unchanged", { tenantKey: resolvedTenantKey2 });
3035
2840
  }
3036
- 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
+ }
3037
2852
  setStorage(getStorageKey("tenant_key"), resolvedTenantKey2);
3038
2853
  } else {
3039
2854
  logger.debug("[AuthProvider] No tenant code determined");
@@ -3232,17 +3047,29 @@ function AuthCallback({
3232
3047
  setStatus("loading");
3233
3048
  setMessage("Completing sign-in...");
3234
3049
  const handled = await handleCallback();
3235
- if (handled) {
3236
- const token = getAccessToken();
3237
- if (token) setTokens(token);
3238
- setStatus("success");
3239
- setMessage("Authentication successful. Redirecting...");
3240
- window.location.replace(successPath);
3241
- } else {
3050
+ if (!handled) {
3242
3051
  setStatus("error");
3243
3052
  setMessage("Authentication failed. Please try again.");
3244
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));
3245
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);
3246
3073
  };
3247
3074
  runCallback();
3248
3075
  }, [homePath, loginPath, successPath, setTokens]);
@@ -3444,6 +3271,7 @@ exports.enforceHttps = enforceHttps;
3444
3271
  exports.ensureValidAccessToken = ensureValidAccessToken;
3445
3272
  exports.extractTenantKey = extractTenantKey;
3446
3273
  exports.getAccessToken = getAccessToken2;
3274
+ exports.getAuthConfig = getAuthConfig;
3447
3275
  exports.getAuthRuntime = getAuthRuntime;
3448
3276
  exports.getConfig = getConfig;
3449
3277
  exports.getStorageAdapter = getStorageAdapter;