tenneo-auth-plugin 1.0.0 → 1.0.3

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
@@ -432,51 +373,7 @@ function createCookieStorageAdapter(namespace, options) {
432
373
  };
433
374
  }
434
375
 
435
- // src/infrastructure/storage/api.ts
436
- var KEY_SUFFIXES = {
437
- authServerUrl: "auth_server_url",
438
- clientId: "client_id",
439
- redirectUri: "redirect_uri",
440
- tenantId: "tenant_id",
441
- tenantKey: "tenant_key",
442
- codeVerifier: "code_verifier",
443
- state: "state",
444
- accessToken: "access_token",
445
- refreshToken: "refresh_token",
446
- expiresAt: "expires_at"
447
- };
448
- 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
- get tenantKey() {
462
- return getStorageKey(KEY_SUFFIXES.tenantKey);
463
- },
464
- get codeVerifier() {
465
- return getStorageKey(KEY_SUFFIXES.codeVerifier);
466
- },
467
- get state() {
468
- return getStorageKey(KEY_SUFFIXES.state);
469
- },
470
- get accessToken() {
471
- return getStorageKey(KEY_SUFFIXES.accessToken);
472
- },
473
- get refreshToken() {
474
- return getStorageKey(KEY_SUFFIXES.refreshToken);
475
- },
476
- get expiresAt() {
477
- return getStorageKey(KEY_SUFFIXES.expiresAt);
478
- }
479
- };
376
+ // src/infrastructure/storage/kv.ts
480
377
  function keyToSuffix(key) {
481
378
  const prefix = getStorageKeyPrefix();
482
379
  return key.startsWith(prefix) ? key.slice(prefix.length) : key;
@@ -512,23 +409,53 @@ function removeStorage(key) {
512
409
  } catch {
513
410
  }
514
411
  }
515
- function clearAuthStorage() {
516
- const adapter = getStorageAdapter();
517
- if (!adapter?.clearSync) return;
518
- try {
519
- adapter.clearTokensSync?.();
520
- adapter.clearSync();
521
- } catch {
412
+
413
+ // src/infrastructure/storage/api.ts
414
+ var KEY_SUFFIXES = {
415
+ tenantKey: "tenant_key",
416
+ codeVerifier: "code_verifier",
417
+ state: "state",
418
+ accessToken: "access_token",
419
+ refreshToken: "refresh_token",
420
+ expiresAt: "expires_at"
421
+ };
422
+ var STORAGE_KEYS = {
423
+ get tenantKey() {
424
+ return getStorageKey(KEY_SUFFIXES.tenantKey);
425
+ },
426
+ get codeVerifier() {
427
+ return getStorageKey(KEY_SUFFIXES.codeVerifier);
428
+ },
429
+ get state() {
430
+ return getStorageKey(KEY_SUFFIXES.state);
431
+ },
432
+ get accessToken() {
433
+ return getStorageKey(KEY_SUFFIXES.accessToken);
434
+ },
435
+ get refreshToken() {
436
+ return getStorageKey(KEY_SUFFIXES.refreshToken);
437
+ },
438
+ get expiresAt() {
439
+ return getStorageKey(KEY_SUFFIXES.expiresAt);
522
440
  }
441
+ };
442
+ function clearAuthStorage() {
443
+ const keysToClear = [
444
+ STORAGE_KEYS.accessToken,
445
+ STORAGE_KEYS.refreshToken,
446
+ STORAGE_KEYS.expiresAt,
447
+ STORAGE_KEYS.codeVerifier,
448
+ STORAGE_KEYS.state,
449
+ getStorageKey("callback_processed"),
450
+ getStorageKey("props_config"),
451
+ getStorageKey("tenant_switched"),
452
+ getStorageKey("new_tenant_key"),
453
+ getStorageKey("preserved_tenant_key")
454
+ ];
455
+ for (const key of keysToClear) removeStorage(key);
523
456
  }
524
457
  function clearAuthSessionStorage() {
525
- const adapter = getStorageAdapter();
526
- if (!adapter?.clearSync) return;
527
- try {
528
- adapter.clearTokensSync?.();
529
- adapter.clearSync();
530
- } catch {
531
- }
458
+ clearAuthStorage();
532
459
  }
533
460
  function clearAllCookies() {
534
461
  const runtime = getAuthRuntime();
@@ -537,27 +464,8 @@ function clearAllCookies() {
537
464
  } catch {
538
465
  }
539
466
  }
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
467
  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
468
  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
469
  clearAllCookies();
562
470
  }
563
471
  function getAccessToken() {
@@ -593,15 +501,6 @@ function isAccessTokenExpired() {
593
501
  const buffer = 60 * 1e3;
594
502
  return Date.now() >= expiresAt - buffer;
595
503
  }
596
- function getClientId() {
597
- return getStorage(STORAGE_KEYS.clientId);
598
- }
599
- function getRedirectUri() {
600
- return getStorage(STORAGE_KEYS.redirectUri);
601
- }
602
- function getTenantId() {
603
- return getStorage(STORAGE_KEYS.tenantId);
604
- }
605
504
  function getCodeVerifier() {
606
505
  return getStorage(STORAGE_KEYS.codeVerifier);
607
506
  }
@@ -662,6 +561,252 @@ var Logger = class {
662
561
  };
663
562
  var logger = new Logger();
664
563
 
564
+ // src/config/resolver-core.ts
565
+ var inMemoryConfig = {};
566
+ var hostConfigProvider = null;
567
+ var CACHE_MARKER = "__tenneo_host_config_cache";
568
+ var CACHE_AT = "__tenneo_host_config_cache_at";
569
+ function setConfig(overrides) {
570
+ inMemoryConfig = { ...inMemoryConfig, ...overrides };
571
+ }
572
+ function clearConfigOverrides() {
573
+ inMemoryConfig = {};
574
+ }
575
+ function setHostConfigProvider(fn) {
576
+ hostConfigProvider = fn;
577
+ }
578
+ function readHostBridge() {
579
+ const out = {};
580
+ try {
581
+ if (typeof window !== "undefined" && window.__TENNEO_AUTH_HOST__) {
582
+ Object.assign(out, window.__TENNEO_AUTH_HOST__);
583
+ }
584
+ } catch {
585
+ }
586
+ try {
587
+ const fromProvider = hostConfigProvider?.();
588
+ if (fromProvider && typeof fromProvider === "object") {
589
+ Object.assign(out, fromProvider);
590
+ }
591
+ } catch (e) {
592
+ logger.warn("[config] hostConfigProvider threw", {
593
+ error: e instanceof Error ? e.message : String(e)
594
+ });
595
+ }
596
+ return out;
597
+ }
598
+ function readStorageConfigCache() {
599
+ try {
600
+ const raw = getStorage(getStorageKey("host_config"));
601
+ if (!raw) return {};
602
+ const parsed = JSON.parse(raw);
603
+ if (!parsed || typeof parsed !== "object") return {};
604
+ const { [CACHE_MARKER]: _m, [CACHE_AT]: _t, ...rest } = parsed;
605
+ if (!rest.clientId && typeof rest.client_id === "string" && rest.client_id.trim()) {
606
+ rest.clientId = rest.client_id.trim();
607
+ }
608
+ return rest;
609
+ } catch {
610
+ return {};
611
+ }
612
+ }
613
+ function resolveSDKConfig(overrides) {
614
+ const stored = readStorageConfigCache();
615
+ const bridge = readHostBridge();
616
+ return {
617
+ ...stored,
618
+ ...bridge,
619
+ ...inMemoryConfig,
620
+ ...overrides ?? {}
621
+ };
622
+ }
623
+ function nonEmptyString(v) {
624
+ if (v == null) return void 0;
625
+ const s = String(v).trim();
626
+ return s || void 0;
627
+ }
628
+ function getConfigFieldSources(overrides) {
629
+ const stored = readStorageConfigCache();
630
+ const bridge = readHostBridge();
631
+ const layers = [
632
+ { src: "storage_cache", cfg: stored },
633
+ { src: "host_bridge", cfg: bridge },
634
+ { src: "memory", cfg: inMemoryConfig }
635
+ ];
636
+ if (overrides && Object.keys(overrides).length) {
637
+ layers.push({ src: "override", cfg: overrides });
638
+ }
639
+ const pick = (key) => {
640
+ for (let i = layers.length - 1; i >= 0; i--) {
641
+ const v = nonEmptyString(layers[i].cfg[key]);
642
+ if (v !== void 0) return layers[i].src;
643
+ }
644
+ return void 0;
645
+ };
646
+ return {
647
+ tenantCode: pick("tenantCode") ?? pick("tenantKey"),
648
+ tenantKey: pick("tenantKey") ?? pick("tenantCode"),
649
+ apiBaseUrl: pick("apiBaseUrl"),
650
+ authServerUrl: pick("authServerUrl"),
651
+ callbackUrl: pick("callbackUrl"),
652
+ clientCode: pick("clientCode"),
653
+ clientId: pick("clientId"),
654
+ tenantId: pick("tenantId"),
655
+ appId: pick("appId")
656
+ };
657
+ }
658
+ function getEffectiveTenantCode(overrides) {
659
+ const cfg = resolveSDKConfig(overrides);
660
+ const t = nonEmptyString(cfg.tenantCode) ?? nonEmptyString(cfg.tenantKey) ?? nonEmptyString(cfg.clientCode) ?? "";
661
+ return t;
662
+ }
663
+ var getEffectiveTenantKey = getEffectiveTenantCode;
664
+ function trim(v) {
665
+ return v == null ? "" : String(v).trim();
666
+ }
667
+ function toResolvedConfig(cfg) {
668
+ const merged = { ...cfg };
669
+ if (!merged.tenantCode && merged.tenantKey) {
670
+ merged.tenantCode = merged.tenantKey;
671
+ }
672
+ const clientId = trim(merged.clientId) || trim(merged.clientCode);
673
+ return {
674
+ apiBaseUrl: trim(merged.apiBaseUrl),
675
+ authServerUrl: trim(merged.authServerUrl),
676
+ callbackUrl: trim(merged.callbackUrl) || trim(merged.redirectUri),
677
+ clientCode: trim(merged.clientCode),
678
+ tenantCode: trim(merged.tenantCode),
679
+ appId: trim(merged.appId),
680
+ clientId,
681
+ tenantId: trim(merged.tenantId)
682
+ };
683
+ }
684
+ var AuthConfigError = class extends Error {
685
+ constructor(missing, message) {
686
+ super(
687
+ message ?? `[tenneo-auth] Missing required auth configuration: ${missing.join(", ")}. Provide authServerUrl, callbackUrl (redirectUri), and tenant_key/tenant_code via AuthProvider/initPlugin or window.__TENNEO_AUTH_HOST__.`
688
+ );
689
+ this.code = "AUTH_CONFIG_MISSING";
690
+ this.name = "AuthConfigError";
691
+ this.missing = missing;
692
+ }
693
+ };
694
+ function resolveConfig(overrides) {
695
+ return toResolvedConfig(resolveSDKConfig(overrides));
696
+ }
697
+ function getAuthConfig(options) {
698
+ const resolved = toResolvedConfig(resolveSDKConfig(options?.overrides));
699
+ if (options?.strict) {
700
+ const missing = [];
701
+ if (!resolved.authServerUrl) missing.push("authServerUrl");
702
+ if (!resolved.callbackUrl) missing.push("callbackUrl");
703
+ if (!resolved.tenantCode) missing.push("tenantCode|tenantKey");
704
+ if (missing.length) throw new AuthConfigError(missing);
705
+ }
706
+ return resolved;
707
+ }
708
+ function syncHostConfigCacheFromMemory() {
709
+ const c = resolveSDKConfig();
710
+ const patch = {};
711
+ const keys = [
712
+ "apiBaseUrl",
713
+ "authServerUrl",
714
+ "callbackUrl",
715
+ "redirectUri",
716
+ "tenantCode",
717
+ "tenantKey",
718
+ "clientCode",
719
+ "clientId",
720
+ "tenantId",
721
+ "appId",
722
+ "basePath"
723
+ ];
724
+ for (const k of keys) {
725
+ const v = c[k];
726
+ if (v != null && String(v).trim()) {
727
+ patch[k] = String(v).trim();
728
+ }
729
+ }
730
+ if (Object.keys(patch).length) writeHostConfigCache(patch);
731
+ }
732
+ function writeHostConfigCache(partial) {
733
+ try {
734
+ const key = getStorageKey("host_config");
735
+ const raw = getStorage(key);
736
+ let existing = {};
737
+ try {
738
+ if (raw) existing = JSON.parse(raw);
739
+ } catch {
740
+ existing = {};
741
+ }
742
+ const next = {
743
+ ...existing,
744
+ ...partial,
745
+ [CACHE_MARKER]: true,
746
+ [CACHE_AT]: (/* @__PURE__ */ new Date()).toISOString()
747
+ };
748
+ setStorage(key, JSON.stringify(next));
749
+ logger.debug("[config] host_config cache updated (non-authoritative)", {
750
+ keys: Object.keys(partial)
751
+ });
752
+ } catch (e) {
753
+ logger.warn("[config] Failed to write host_config cache", {
754
+ error: e instanceof Error ? e.message : String(e)
755
+ });
756
+ }
757
+ }
758
+ function initPlugin(config, opts) {
759
+ setConfig(config);
760
+ logger.info("[config] initPlugin \u2014 runtime config updated", {
761
+ syncStorageCache: opts?.syncStorageCache !== false,
762
+ tenantCode: config.tenantCode ?? config.tenantKey ?? "(none)"
763
+ });
764
+ if (opts?.syncStorageCache !== false) {
765
+ writeHostConfigCache(config);
766
+ }
767
+ }
768
+ function logConfigResolutionDebug(context, overrides) {
769
+ const sources = getConfigFieldSources(overrides);
770
+ logger.debug(`[config] ${context}`, {
771
+ fieldSources: sources,
772
+ effectiveTenantCode: getEffectiveTenantCode(overrides) || "(empty)",
773
+ hasAuthServerUrl: !!resolveSDKConfig(overrides).authServerUrl,
774
+ hasCallbackUrl: !!resolveSDKConfig(overrides).callbackUrl
775
+ });
776
+ }
777
+
778
+ // src/config/host-fields.ts
779
+ function trim2(v) {
780
+ if (v == null) return null;
781
+ const s = String(v).trim();
782
+ return s || null;
783
+ }
784
+ function oauthClientIdForTokenRequest(raw) {
785
+ if (!raw) return null;
786
+ let s = raw.trim();
787
+ if (!s) return null;
788
+ if (s.toLowerCase().startsWith("public-")) {
789
+ s = s.slice("public-".length).trim();
790
+ }
791
+ return s || null;
792
+ }
793
+ function getClientId() {
794
+ const c = resolveSDKConfig();
795
+ const fromId = oauthClientIdForTokenRequest(trim2(c.clientId));
796
+ if (fromId) return fromId;
797
+ return oauthClientIdForTokenRequest(trim2(c.clientCode));
798
+ }
799
+ function normalizeClientIdForOAuthTokenBody(raw) {
800
+ return oauthClientIdForTokenRequest(trim2(raw));
801
+ }
802
+ function getRedirectUri() {
803
+ const c = resolveSDKConfig();
804
+ return trim2(c.callbackUrl) ?? trim2(c.redirectUri);
805
+ }
806
+ function getTenantId() {
807
+ return trim2(resolveSDKConfig().tenantId);
808
+ }
809
+
665
810
  // src/domain/errors.ts
666
811
  function isLocalhostHostname(hostname) {
667
812
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.startsWith("172.16.");
@@ -717,10 +862,8 @@ function resolveAuthLifecycleState(input) {
717
862
  hasRefreshToken,
718
863
  isTokenExpired
719
864
  } = input;
720
- const forceLogin = getStorage3("force_login") === "true";
721
865
  const callbackProcessed = getStorage3("callback_processed") === "true";
722
866
  const tenantSwitched = getStorage3("tenant_switched") === "true";
723
- if (forceLogin) return "FORCE_LOGIN" /* FORCE_LOGIN */;
724
867
  if (isCallbackUrl2(url)) return "PROCESSING_CALLBACK" /* PROCESSING_CALLBACK */;
725
868
  if (hasValidToken) return "AUTHENTICATED" /* AUTHENTICATED */;
726
869
  if (callbackProcessed && !hasToken) return "LOGIN_REQUIRED" /* LOGIN_REQUIRED */;
@@ -781,30 +924,58 @@ function setFlowFlags() {
781
924
  setFlowInProgress();
782
925
  }
783
926
 
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, "");
927
+ // src/application/events/AuthEvents.ts
928
+ var AuthEventNames = {
929
+ LOGIN_STARTED: "LOGIN_STARTED",
930
+ LOGIN_SUCCESS: "LOGIN_SUCCESS",
931
+ LOGIN_FAILED: "LOGIN_FAILED",
932
+ CALLBACK_SUCCESS: "CALLBACK_SUCCESS",
933
+ CALLBACK_FAILED: "CALLBACK_FAILED",
934
+ TOKEN_REFRESH_SUCCESS: "TOKEN_REFRESH_SUCCESS",
935
+ TOKEN_REFRESH_FAILED: "TOKEN_REFRESH_FAILED",
936
+ TENANT_SWITCHED: "TENANT_SWITCHED",
937
+ LOGOUT_COMPLETED: "LOGOUT_COMPLETED",
938
+ BOOTSTRAP_STATE_DETECTED: "BOOTSTRAP_STATE_DETECTED",
939
+ REDIRECT_LOOP_RISK: "REDIRECT_LOOP_RISK"
940
+ };
941
+ var listeners = /* @__PURE__ */ new Map();
942
+ function getHandlers(event) {
943
+ let set = listeners.get(event);
944
+ if (!set) {
945
+ set = /* @__PURE__ */ new Set();
946
+ listeners.set(event, set);
947
+ }
948
+ return set;
790
949
  }
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");
950
+ function getWildcardHandlers() {
951
+ let set = listeners.get("*");
952
+ if (!set) {
953
+ set = /* @__PURE__ */ new Set();
954
+ listeners.set("*", set);
801
955
  }
956
+ return set;
802
957
  }
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, "");
958
+ function emitAuthEvent(event, payload) {
959
+ const handlers = getHandlers(event);
960
+ const wildcard = getWildcardHandlers();
961
+ const p = payload ?? {};
962
+ handlers.forEach((fn) => {
963
+ try {
964
+ fn(p);
965
+ } catch {
966
+ }
967
+ });
968
+ wildcard.forEach((fn) => {
969
+ try {
970
+ fn({ ...p, event });
971
+ } catch {
972
+ }
973
+ });
974
+ }
975
+ function subscribeAuthEvent(event, handler) {
976
+ const set = event === "*" ? getWildcardHandlers() : getHandlers(event);
977
+ set.add(handler);
978
+ return () => set.delete(handler);
808
979
  }
809
980
 
810
981
  // src/config/constants.ts
@@ -834,342 +1005,169 @@ var DEFAULT_TOKEN_GRANT_TYPE = OAUTH_CONSTANTS.TOKEN_GRANT_TYPE;
834
1005
  var DEFAULT_REFRESH_GRANT_TYPE = OAUTH_CONSTANTS.REFRESH_GRANT_TYPE;
835
1006
 
836
1007
  // src/config/index.ts
837
- var inMemoryConfig = {};
838
- function readHostConfigFromStorage() {
839
- try {
840
- const raw = getStorage(getStorageKey("host_config"));
841
- if (!raw) return {};
842
- const parsed = JSON.parse(raw);
843
- return parsed && typeof parsed === "object" ? parsed : {};
844
- } catch {
845
- return {};
846
- }
1008
+ function getAuthConfig2(options) {
1009
+ return getAuthConfig(options);
847
1010
  }
848
- function resolveMerged(overrides) {
849
- const stored = readHostConfigFromStorage();
850
- const cfg = { ...stored, ...inMemoryConfig, ...overrides ?? {} };
851
- const trimmed = (v) => v == null ? "" : String(v).trim();
852
- return {
853
- apiBaseUrl: trimmed(cfg.apiBaseUrl),
854
- authServerUrl: trimmed(cfg.authServerUrl),
855
- callbackUrl: trimmed(cfg.callbackUrl),
856
- clientCode: trimmed(cfg.clientCode)
857
- };
858
- }
859
- function resolveConfig(overrides) {
860
- return resolveMerged(overrides);
1011
+ function requireAuthConfig(overrides) {
1012
+ return getAuthConfig({ strict: true, overrides });
861
1013
  }
862
1014
  function getConfig() {
863
- return resolveMerged();
864
- }
865
- function setConfig(overrides) {
866
- inMemoryConfig = { ...inMemoryConfig, ...overrides };
867
- }
868
- function clearConfigOverrides() {
869
- inMemoryConfig = {};
1015
+ return resolveConfig();
870
1016
  }
871
1017
  function getRedirectUri2() {
872
- const cfg = resolveMerged();
1018
+ const cfg = resolveConfig();
873
1019
  if (cfg.callbackUrl) return cfg.callbackUrl;
874
1020
  if (typeof window !== "undefined") return `${window.location.origin}/callback`;
875
1021
  return "";
876
1022
  }
877
1023
  var Config = {
878
1024
  getApiBaseUrl() {
879
- return resolveMerged().apiBaseUrl;
1025
+ return resolveConfig().apiBaseUrl;
880
1026
  },
881
1027
  getAuthServerUrl() {
882
- return resolveMerged().authServerUrl;
1028
+ return resolveConfig().authServerUrl;
883
1029
  },
884
1030
  getClientCode() {
885
- return resolveMerged().clientCode;
1031
+ return resolveConfig().clientCode;
886
1032
  },
887
1033
  getRedirectUri() {
888
1034
  return getRedirectUri2();
889
1035
  },
890
1036
  getAll() {
891
- return resolveMerged();
1037
+ return resolveConfig();
892
1038
  }
893
1039
  };
894
-
895
- // src/utils/validation.ts
896
- function isValidGuid(guid) {
897
- if (!guid || typeof guid !== "string") {
898
- return false;
899
- }
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
- }
903
- function validateTenantId(tenantId) {
904
- if (!tenantId || tenantId.trim() === "") {
905
- return {
906
- isValid: false,
907
- error: "Tenant ID is required"
908
- };
909
- }
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
- };
1040
+ function applyRuntimeTenantSwitch(tenantKey, opts) {
1041
+ const t = tenantKey.trim();
1042
+ if (!t) return;
1043
+ setConfig({ tenantCode: t, tenantKey: t });
1044
+ logConfigResolutionDebug("applyRuntimeTenantSwitch", { tenantCode: t, tenantKey: t });
1045
+ if (opts?.syncStorageCache !== false) {
1046
+ writeHostConfigCache({ tenantCode: t, tenantKey: t });
915
1047
  }
916
- return { isValid: true };
917
1048
  }
918
1049
 
919
- // src/application/domain-facade.ts
920
- function isLocalhost2() {
921
- const runtime = getAuthRuntime();
922
- const hostname = runtime ? runtime.urlProvider.getCurrentUrl().hostname : typeof window !== "undefined" ? window.location.hostname : "";
923
- return isLocalhost(hostname);
924
- }
925
- function extractTenantKey() {
926
- const runtime = getAuthRuntime();
927
- if (runtime) {
928
- const url = runtime.urlProvider.getCurrentUrl();
929
- return extractTenantKeyFromUrl(url, runtime.configProvider, getStorage, getStorageKey);
1050
+ // src/infrastructure/tenant/TenantConfigCache.ts
1051
+ var DEFAULT_TTL_MS = 5 * 60 * 1e3;
1052
+ var TenantConfigCache = class {
1053
+ constructor(ttlMs = DEFAULT_TTL_MS) {
1054
+ this.cache = /* @__PURE__ */ new Map();
1055
+ this.inFlight = /* @__PURE__ */ new Map();
1056
+ this.ttlMs = ttlMs;
930
1057
  }
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();
1058
+ get(tenantKey) {
1059
+ const entry = this.cache.get(tenantKey);
1060
+ if (!entry) return null;
1061
+ if (Date.now() >= entry.expiresAt) {
1062
+ this.cache.delete(tenantKey);
1063
+ return null;
940
1064
  }
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();
1065
+ return entry.config;
952
1066
  }
953
- }
954
- function isCallbackUrl() {
955
- const runtime = getAuthRuntime();
956
- if (runtime) return isCallbackUrlFromUrl(runtime.urlProvider.getCurrentUrl());
957
- if (typeof window === "undefined") return false;
958
- const params = new URLSearchParams(window.location.search);
959
- return params.has("code") && params.has("state");
960
- }
961
-
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);
1067
+ set(tenantKey, config) {
1068
+ this.cache.set(tenantKey, {
1069
+ config,
1070
+ expiresAt: Date.now() + this.ttlMs
1071
+ });
982
1072
  }
983
- return set;
984
- }
985
- function getWildcardHandlers() {
986
- let set = listeners.get("*");
987
- if (!set) {
988
- set = /* @__PURE__ */ new Set();
989
- listeners.set("*", set);
1073
+ invalidate(tenantKey) {
1074
+ if (tenantKey) {
1075
+ this.cache.delete(tenantKey);
1076
+ this.inFlight.delete(tenantKey);
1077
+ } else {
1078
+ this.cache.clear();
1079
+ this.inFlight.clear();
1080
+ }
990
1081
  }
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 {
1082
+ async getOrResolve(tenantKey, resolver) {
1083
+ if (!tenantKey || !tenantKey.trim()) return null;
1084
+ const key = tenantKey.trim();
1085
+ const cached = this.get(key);
1086
+ if (cached) {
1087
+ logger.debug("Tenant config from cache", { tenantKey: key });
1088
+ return cached;
1001
1089
  }
1002
- });
1003
- wildcard.forEach((fn) => {
1004
- try {
1005
- fn({ ...p, event });
1006
- } catch {
1090
+ const existing = this.inFlight.get(key);
1091
+ if (existing) {
1092
+ logger.debug("Tenant resolution reused (in-flight)", { tenantKey: key });
1093
+ return existing;
1007
1094
  }
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;
1095
+ const promise = (async () => {
1096
+ try {
1097
+ const config = await resolver(key);
1098
+ if (config) this.set(key, config);
1099
+ return config;
1100
+ } finally {
1101
+ this.inFlight.delete(key);
1102
+ }
1103
+ })();
1104
+ this.inFlight.set(key, promise);
1105
+ return promise;
1036
1106
  }
1037
- redirectCount++;
1038
- return true;
1039
- }
1107
+ };
1040
1108
 
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) {
1109
+ // src/infrastructure/tenant/tenantResolution.ts
1110
+ var defaultTenantConfigCache = new TenantConfigCache();
1111
+ function consumePropsConfig() {
1112
+ const propsConfigKey = getStorageKey("props_config");
1057
1113
  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;
1114
+ const propsConfigStr = getStorage(propsConfigKey);
1115
+ if (!propsConfigStr) return null;
1116
+ const parsed = JSON.parse(propsConfigStr);
1117
+ return parsed && typeof parsed === "object" ? parsed : null;
1118
+ } catch {
1119
+ return null;
1120
+ } finally {
1121
+ removeStorage(propsConfigKey);
1145
1122
  }
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
- });
1123
+ }
1124
+ function validateTenantKey(value) {
1125
+ if (typeof value !== "string") return null;
1126
+ const trimmed = value.trim();
1127
+ return trimmed ? trimmed : null;
1128
+ }
1129
+ function resolveTenantKey() {
1130
+ const propsConfig = consumePropsConfig();
1131
+ const tenantFromProps = validateTenantKey(propsConfig?.tenantKey);
1132
+ if (tenantFromProps) {
1133
+ logger.debug("[tenant] Resolved tenant from props_config", { tenantKey: tenantFromProps });
1134
+ return tenantFromProps;
1156
1135
  }
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()}`;
1136
+ const fromHost = validateTenantKey(getEffectiveTenantCode());
1137
+ if (fromHost) {
1138
+ logger.debug("[tenant] Resolved tenant from host/runtime config", { tenantKey: fromHost });
1139
+ return fromHost;
1140
+ }
1141
+ logger.warn("[tenant] Unable to resolve tenant key (props + host/runtime empty)");
1142
+ return null;
1143
+ }
1144
+ async function resolveTenantConfig(tenantKey, tenantResolver) {
1145
+ const resolved = await tenantResolver.resolve(tenantKey);
1146
+ if (!resolved) return null;
1147
+ return { ...resolved, tenantKey };
1148
+ }
1149
+ async function resolveTenantConfigCached(tenantKey, tenantResolver) {
1150
+ return defaultTenantConfigCache.getOrResolve(
1151
+ tenantKey,
1152
+ (key) => resolveTenantConfig(key, tenantResolver)
1153
+ );
1154
+ }
1155
+
1156
+ // src/application/domain-facade.ts
1157
+ function isLocalhost2() {
1158
+ const runtime = getAuthRuntime();
1159
+ const hostname = runtime ? runtime.urlProvider.getCurrentUrl().hostname : typeof window !== "undefined" ? window.location.hostname : "";
1160
+ return isLocalhost(hostname);
1161
+ }
1162
+ function extractTenantKey() {
1163
+ return resolveTenantKey();
1164
+ }
1165
+ function isCallbackUrl() {
1166
+ const runtime = getAuthRuntime();
1167
+ if (runtime) return isCallbackUrlFromUrl(runtime.urlProvider.getCurrentUrl());
1168
+ if (typeof window === "undefined") return false;
1169
+ const params = new URLSearchParams(window.location.search);
1170
+ return params.has("code") && params.has("state");
1173
1171
  }
1174
1172
 
1175
1173
  // src/strategies/oauth2-pkce/token-exchange.ts
@@ -1331,6 +1329,221 @@ async function refreshAccessToken() {
1331
1329
  })();
1332
1330
  return refreshPromise;
1333
1331
  }
1332
+
1333
+ // src/strategies/oauth2-pkce/pkce.ts
1334
+ function generateCodeVerifier(length = 128) {
1335
+ const array = new Uint8Array(length);
1336
+ crypto.getRandomValues(array);
1337
+ const base64 = btoa(String.fromCharCode(...array));
1338
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1339
+ }
1340
+ async function generateCodeChallenge(verifier) {
1341
+ try {
1342
+ const encoder = new TextEncoder();
1343
+ const data = encoder.encode(verifier);
1344
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1345
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
1346
+ const base64 = btoa(String.fromCharCode(...hashArray));
1347
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1348
+ } catch {
1349
+ throw new Error("Failed to generate code challenge");
1350
+ }
1351
+ }
1352
+ function generateState() {
1353
+ const array = new Uint8Array(32);
1354
+ crypto.getRandomValues(array);
1355
+ const base64 = btoa(String.fromCharCode(...array));
1356
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1357
+ }
1358
+
1359
+ // src/utils/validation.ts
1360
+ function isValidGuid(guid) {
1361
+ if (!guid || typeof guid !== "string") {
1362
+ return false;
1363
+ }
1364
+ const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1365
+ return guidRegex.test(guid.trim());
1366
+ }
1367
+ function validateTenantId(tenantId) {
1368
+ if (!tenantId || tenantId.trim() === "") {
1369
+ return {
1370
+ isValid: false,
1371
+ error: "Tenant ID is required"
1372
+ };
1373
+ }
1374
+ if (!isValidGuid(tenantId)) {
1375
+ return {
1376
+ isValid: false,
1377
+ error: "Tenant ID must be a valid GUID format (00000000-0000-0000-0000-000000000001)"
1378
+ };
1379
+ }
1380
+ return { isValid: true };
1381
+ }
1382
+
1383
+ // src/application/security/redirectLoopGuard.ts
1384
+ var DEFAULT_MAX_REDIRECTS = 5;
1385
+ var WINDOW_MS = 60 * 1e3;
1386
+ var redirectCount = 0;
1387
+ var firstRedirectAt = 0;
1388
+ function resetRedirectLoopGuard() {
1389
+ redirectCount = 0;
1390
+ firstRedirectAt = 0;
1391
+ }
1392
+ function checkAndIncrementRedirect(maxAttempts = DEFAULT_MAX_REDIRECTS) {
1393
+ const now = Date.now();
1394
+ if (now - firstRedirectAt > WINDOW_MS) {
1395
+ redirectCount = 0;
1396
+ firstRedirectAt = now;
1397
+ }
1398
+ if (redirectCount >= maxAttempts) {
1399
+ emitAuthEvent(AuthEventNames.REDIRECT_LOOP_RISK, {
1400
+ message: `Redirect attempt limit (${maxAttempts}) exceeded`
1401
+ });
1402
+ return false;
1403
+ }
1404
+ redirectCount++;
1405
+ return true;
1406
+ }
1407
+
1408
+ // src/strategies/oauth2-pkce/oauth.ts
1409
+ function storeTenantConfig(config) {
1410
+ const validation = validateTenantId(config.tenantId);
1411
+ if (!validation.isValid) {
1412
+ logger.warn("Tenant ID validation failed", { tenantId: config.tenantId });
1413
+ }
1414
+ const authServerToStore = Config.getAuthServerUrl() || config.authServer;
1415
+ const redirectUriToStore = Config.getRedirectUri();
1416
+ writeHostConfigCache({
1417
+ authServerUrl: authServerToStore,
1418
+ clientId: config.client.clientId,
1419
+ clientCode: config.client.clientCode ?? config.client.clientId,
1420
+ redirectUri: redirectUriToStore,
1421
+ callbackUrl: redirectUriToStore,
1422
+ tenantId: config.tenantId,
1423
+ tenantKey: config.tenantKey ?? "",
1424
+ tenantCode: config.tenantKey ?? ""
1425
+ });
1426
+ if (config.tenantKey) {
1427
+ setStorage(STORAGE_KEYS.tenantKey, config.tenantKey);
1428
+ }
1429
+ }
1430
+ async function initiateAuthFlow(config) {
1431
+ try {
1432
+ logger.info("Initiating OAuth authorization flow");
1433
+ const authServerFromProps = Config.getAuthServerUrl();
1434
+ const authServerEffective = (authServerFromProps || config?.authServer || "").replace(/\/+$/, "");
1435
+ const redirectUriEffective = Config.getRedirectUri();
1436
+ const tenantIdEffective = config?.tenantId || "";
1437
+ const clientIdEffective = config?.client?.clientId || "";
1438
+ logger.debug("[initiateAuthFlow] Resolved inputs", {
1439
+ authServerFromProps: authServerFromProps ? "(provided)" : "(empty)",
1440
+ authServerEffective,
1441
+ redirectUriEffective,
1442
+ tenantIdEffective,
1443
+ clientIdEffective,
1444
+ scopes: config?.client?.scopes,
1445
+ hasRuntime: !!getAuthRuntime()
1446
+ });
1447
+ const token = getAccessToken();
1448
+ if (token && !isAccessTokenExpired()) {
1449
+ logger.debug("Valid token already exists, skipping auth flow");
1450
+ clearFlowFlags();
1451
+ return;
1452
+ }
1453
+ if (isCallbackUrl()) {
1454
+ logger.debug("Callback URL detected - clearing flow flag to allow callback processing");
1455
+ clearFlowFlags();
1456
+ return;
1457
+ }
1458
+ if (isFlowInProgress()) {
1459
+ if (isFlowFlagStale(3e3)) {
1460
+ logger.info("Stale auth flow flag detected, clearing and proceeding");
1461
+ clearFlowFlags();
1462
+ } else {
1463
+ logger.debug("Auth flow already in progress (recent), skipping duplicate redirect");
1464
+ return;
1465
+ }
1466
+ }
1467
+ setFlowFlags();
1468
+ if (!(Config.getAuthServerUrl() || config?.authServer) || !config.client?.clientId || !config.client?.redirectUri) {
1469
+ logger.error("[initiateAuthFlow] Missing required tenant config", {
1470
+ authServerEffective,
1471
+ clientId: config?.client?.clientId,
1472
+ redirectUriFromTenantApi: config?.client?.redirectUri,
1473
+ redirectUriEffective
1474
+ });
1475
+ throw new Error("Invalid tenant configuration for auth flow");
1476
+ }
1477
+ storeTenantConfig(config);
1478
+ logger.debug("[initiateAuthFlow] Stored tenant config, generating PKCE");
1479
+ removeStorage(getStorageKey("callback_processed"));
1480
+ removeStorage(getStorageKey("state_mismatch"));
1481
+ clearCallbackProcessing();
1482
+ const codeVerifier = generateCodeVerifier();
1483
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
1484
+ const state = generateState();
1485
+ removeStorage(STORAGE_KEYS.codeVerifier);
1486
+ removeStorage(STORAGE_KEYS.state);
1487
+ setStorage(STORAGE_KEYS.codeVerifier, codeVerifier);
1488
+ setStorage(STORAGE_KEYS.state, state);
1489
+ const authUrl = buildAuthorizationUrl(config, codeChallenge, state);
1490
+ logger.debug("[initiateAuthFlow] Built authorization URL", {
1491
+ authServerEffective,
1492
+ urlPrefix: authUrl?.slice(0, 80)
1493
+ });
1494
+ if (!authUrl || !(authUrl.startsWith("http://") || authUrl.startsWith("https://"))) {
1495
+ throw new Error("Invalid authorization URL generated");
1496
+ }
1497
+ if (!checkAndIncrementRedirect()) {
1498
+ logger.warn("Redirect loop protection: max redirect attempts exceeded");
1499
+ clearFlowFlags();
1500
+ return;
1501
+ }
1502
+ logger.info("Redirecting to authorization server");
1503
+ emitAuthEvent(AuthEventNames.LOGIN_STARTED, { tenantKey: config.tenantKey });
1504
+ const runtime = getAuthRuntime();
1505
+ if (runtime) {
1506
+ logger.debug("[initiateAuthFlow] Redirect via runtime.urlProvider.redirect");
1507
+ runtime.urlProvider.redirect(authUrl);
1508
+ } else if (typeof window !== "undefined") {
1509
+ logger.debug("[initiateAuthFlow] Redirect via window.location.replace");
1510
+ window.location.replace(authUrl);
1511
+ } else {
1512
+ logger.error("[initiateAuthFlow] No runtime and no window; cannot redirect", { authUrl });
1513
+ }
1514
+ } catch (error) {
1515
+ clearFlowFlags();
1516
+ logger.error("Failed to initiate auth flow", {
1517
+ error: error instanceof Error ? error.message : String(error)
1518
+ });
1519
+ throw error;
1520
+ }
1521
+ }
1522
+ function buildAuthorizationUrl(config, codeChallenge, state) {
1523
+ const authServerBase = (Config.getAuthServerUrl() || config.authServer || "").replace(/\/+$/, "");
1524
+ if (isLocalhost2()) {
1525
+ logger.debug("Building authorization URL", {
1526
+ authServer: authServerBase,
1527
+ clientId: config.client.clientId,
1528
+ redirectUri: config.client.redirectUri,
1529
+ scopes: config.client.scopes
1530
+ });
1531
+ }
1532
+ const tenantIdToUse = config.tenantId;
1533
+ const clientIdToUse = config.client.clientId;
1534
+ const redirectUriToUse = Config.getRedirectUri();
1535
+ const params = new URLSearchParams({
1536
+ client_id: `public-${clientIdToUse}`,
1537
+ tenant_id: tenantIdToUse,
1538
+ redirect_uri: redirectUriToUse,
1539
+ response_type: DEFAULT_RESPONSE_TYPE,
1540
+ scope: config.client.scopes,
1541
+ state,
1542
+ code_challenge: codeChallenge,
1543
+ code_challenge_method: DEFAULT_CODE_CHALLENGE_METHOD
1544
+ });
1545
+ return `${authServerBase}/connect/authorize?${params.toString()}`;
1546
+ }
1334
1547
  function isProcessed() {
1335
1548
  return getStorage(getStorageKey("callback_processed")) === "true";
1336
1549
  }
@@ -1349,15 +1562,6 @@ function clearProcessing() {
1349
1562
  removeStorage(getStorageKey("callback_processed"));
1350
1563
  }
1351
1564
  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
1565
  function cleanUrlSoft() {
1362
1566
  if (urlCleaned) return;
1363
1567
  if (isProcessing()) return;
@@ -1368,13 +1572,52 @@ function cleanUrlSoft() {
1368
1572
  urlCleaned = true;
1369
1573
  }
1370
1574
  function clearAllAuthFlags() {
1371
- removeStorage(getStorageKey("force_login"));
1372
- removeStorage(getStorageKey("logout_in_progress"));
1373
1575
  clearFlowFlags();
1374
1576
  }
1577
+ async function tryRecoverOAuthConfigFromTenant() {
1578
+ const runtime = getAuthRuntime();
1579
+ const tenantKey = (resolveTenantKey() ?? getTenantKey() ?? "").trim();
1580
+ if (!runtime?.tenantResolver || !tenantKey) {
1581
+ logger.debug("[Callback] OAuth config recovery skipped \u2014 no tenantResolver or tenantKey", {
1582
+ hasResolver: !!runtime?.tenantResolver,
1583
+ tenantKey: tenantKey || "(empty)"
1584
+ });
1585
+ return null;
1586
+ }
1587
+ try {
1588
+ const tc = await runtime.tenantResolver.resolve(tenantKey);
1589
+ if (!tc?.client?.clientId || !tc.tenantId) {
1590
+ logger.warn("[Callback] OAuth config recovery: tenant resolve missing client or tenantId", {
1591
+ tenantKey
1592
+ });
1593
+ return null;
1594
+ }
1595
+ storeTenantConfig(tc);
1596
+ return {
1597
+ clientId: tc.client.clientId.trim() || null,
1598
+ redirectUri: tc.client.redirectUri?.trim() || getRedirectUri(),
1599
+ tenantId: tc.tenantId.trim() || null
1600
+ };
1601
+ } catch (e) {
1602
+ logger.warn("[Callback] OAuth config recovery: tenant re-resolve failed", {
1603
+ tenantKey,
1604
+ error: e instanceof Error ? e.message : String(e)
1605
+ });
1606
+ return null;
1607
+ }
1608
+ }
1375
1609
  async function handleCallback() {
1610
+ const runtimeSearch = getAuthRuntime()?.urlProvider.getCurrentUrl().search;
1611
+ const windowSearch = typeof window !== "undefined" ? window.location.search : "";
1612
+ logger.debug("[Callback] handleCallback invoked", {
1613
+ isProcessed: isProcessed(),
1614
+ isProcessing: isProcessing(),
1615
+ isCallbackUrl: isCallbackUrl(),
1616
+ runtimeSearch: runtimeSearch ?? "(none)",
1617
+ windowSearch: windowSearch || "(none)"
1618
+ });
1376
1619
  if (isProcessed()) {
1377
- logger.debug("Callback already processed in this session - preventing duplicate processing");
1620
+ logger.warn("[Callback] Early exit: callback already processed");
1378
1621
  if (isCallbackUrl()) {
1379
1622
  if (!isProcessing()) cleanUrlSoft();
1380
1623
  }
@@ -1382,31 +1625,16 @@ async function handleCallback() {
1382
1625
  return true;
1383
1626
  }
1384
1627
  if (isProcessing()) {
1385
- logger.debug("Callback processing already in progress - waiting for completion");
1628
+ logger.warn("[Callback] Early exit: callback processing already in progress");
1386
1629
  return true;
1387
1630
  }
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
1631
  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;
1632
+ const currentTenantKey = resolveTenantKey() ?? storedTenantKey;
1408
1633
  if (currentTenantKey && storedTenantKey && currentTenantKey !== storedTenantKey) {
1409
- logger.info("TenantKey changed - bypassing callback, starting fresh login");
1634
+ logger.warn("[Callback] Early exit: tenant key changed before exchange", {
1635
+ currentTenantKey,
1636
+ storedTenantKey
1637
+ });
1410
1638
  clearAllStorage();
1411
1639
  clearAllAuthFlags();
1412
1640
  clearProcessing();
@@ -1416,7 +1644,10 @@ async function handleCallback() {
1416
1644
  if (isCallbackUrl()) cleanUrlSoft();
1417
1645
  return false;
1418
1646
  }
1419
- if (!isCallbackUrl()) return false;
1647
+ if (!isCallbackUrl()) {
1648
+ logger.warn("[Callback] Early exit: URL is not callback URL");
1649
+ return false;
1650
+ }
1420
1651
  markProcessing();
1421
1652
  clearFlowFlags();
1422
1653
  urlCleaned = false;
@@ -1427,70 +1658,76 @@ async function handleCallback() {
1427
1658
  const state = params.get("state");
1428
1659
  const error = params.get("error");
1429
1660
  if (error || !code || !state) {
1430
- logger.error("Invalid OAuth callback params", { error, code, state });
1661
+ logger.error("[Callback] Early exit: invalid OAuth callback params", { error, code, state });
1431
1662
  clearAllAuthFlags();
1432
1663
  clearProcessing();
1433
- markProcessed();
1434
1664
  cleanUrlSoft();
1435
1665
  return false;
1436
1666
  }
1437
1667
  const storedState = getState();
1438
1668
  if (!storedState || storedState !== state) {
1439
1669
  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;
1670
+ const resolvedTenantKey = resolveTenantKey();
1671
+ const tenantKeyChanged = currentTenantKeyFromUrl && resolvedTenantKey && currentTenantKeyFromUrl !== resolvedTenantKey;
1450
1672
  if (tenantKeyChanged) {
1451
- logger.info("OAuth state mismatch due to tenantKey change - expected behavior, starting fresh login");
1673
+ logger.warn("[Callback] Early exit: state mismatch due to tenant key change");
1452
1674
  } else {
1453
- logger.error("OAuth state mismatch - redirecting to login");
1675
+ logger.error("[Callback] Early exit: OAuth state mismatch - redirecting to login");
1454
1676
  }
1455
1677
  clearAllAuthFlags();
1456
1678
  clearProcessing();
1457
- markProcessed();
1458
1679
  cleanUrlSoft();
1459
1680
  setStorage(getStorageKey("state_mismatch"), "true");
1460
1681
  return false;
1461
1682
  }
1462
1683
  const codeVerifier = getCodeVerifier();
1463
1684
  if (!codeVerifier) {
1464
- logger.error("Missing PKCE verifier");
1685
+ logger.error("[Callback] Early exit: missing PKCE verifier");
1465
1686
  clearAllAuthFlags();
1466
1687
  clearProcessing();
1467
- markProcessed();
1468
1688
  cleanUrlSoft();
1469
1689
  return false;
1470
1690
  }
1471
- const clientId = getClientId();
1472
- const redirectUri = getRedirectUri();
1473
- const tenantId = getTenantId();
1691
+ let clientId = getClientId();
1692
+ let redirectUri = getRedirectUri();
1693
+ let tenantId = getTenantId();
1694
+ if (!clientId || !redirectUri || !tenantId) {
1695
+ logger.warn("[Callback] OAuth config incomplete \u2014 attempting tenant re-resolve", {
1696
+ hasClientId: !!clientId,
1697
+ hasRedirectUri: !!redirectUri,
1698
+ hasTenantId: !!tenantId
1699
+ });
1700
+ const recovered = await tryRecoverOAuthConfigFromTenant();
1701
+ if (recovered) {
1702
+ if (!redirectUri) redirectUri = recovered.redirectUri;
1703
+ if (!tenantId) tenantId = recovered.tenantId;
1704
+ clientId = getClientId() ?? normalizeClientIdForOAuthTokenBody(recovered.clientId) ?? null;
1705
+ }
1706
+ }
1474
1707
  if (!clientId || !redirectUri || !tenantId) {
1475
- logger.error("Missing OAuth config during callback");
1708
+ logger.error("[Callback] Early exit: missing OAuth config during callback", {
1709
+ hasClientId: !!clientId,
1710
+ hasRedirectUri: !!redirectUri,
1711
+ hasTenantId: !!tenantId
1712
+ });
1476
1713
  clearAllAuthFlags();
1477
1714
  clearProcessing();
1478
- markProcessed();
1479
1715
  cleanUrlSoft();
1480
1716
  return false;
1481
1717
  }
1482
1718
  const existingToken = getAccessToken();
1483
1719
  if (existingToken && !isAccessTokenExpired()) {
1484
- logger.debug("Valid token already exists - skipping code exchange");
1720
+ logger.warn("[Callback] Early exit: valid token already exists - skipping code exchange");
1485
1721
  removeStorage(STORAGE_KEYS.codeVerifier);
1486
1722
  removeStorage(STORAGE_KEYS.state);
1487
1723
  clearAllAuthFlags();
1488
1724
  markProcessed();
1489
- cleanUrl();
1725
+ cleanUrlSoft();
1490
1726
  resetRedirectLoopGuard();
1491
1727
  emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
1492
1728
  return true;
1493
1729
  }
1730
+ logger.info("[Callback] Entering token exchange");
1494
1731
  const tokenData = await exchangeCodeForTokens({
1495
1732
  code,
1496
1733
  codeVerifier,
@@ -1510,7 +1747,7 @@ async function handleCallback() {
1510
1747
  removeStorage(STORAGE_KEYS.state);
1511
1748
  clearAllAuthFlags();
1512
1749
  markProcessed();
1513
- cleanUrl();
1750
+ cleanUrlSoft();
1514
1751
  logger.info("OAuth callback processed successfully");
1515
1752
  resetRedirectLoopGuard();
1516
1753
  emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
@@ -1571,157 +1808,6 @@ async function handleCallback() {
1571
1808
  }
1572
1809
  }
1573
1810
 
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
- );
1718
- }
1719
- storeTenantConfig(tenantConfig);
1720
- await initiateAuthFlow(tenantConfig);
1721
- return { completed: true };
1722
- }
1723
- };
1724
-
1725
1811
  // src/application/flows/CallbackFlow.ts
1726
1812
  var CallbackFlow = class {
1727
1813
  async execute(context) {
@@ -1809,21 +1895,16 @@ var RefreshFlow = class {
1809
1895
  error: error instanceof Error ? error.message : String(error)
1810
1896
  });
1811
1897
  }
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();
1898
+ const tenantKey = resolveTenantKey();
1816
1899
  log.debug("[RefreshFlow] Tenant key resolved", {
1817
1900
  tenantKey: tenantKey ?? "(null)",
1818
- fromRuntime: tenantKeyFromRuntime ?? "(null)",
1819
- fromStorage: tenantKeyFromStorage ?? "(null)",
1820
- fromConfig: tenantKeyFromConfig || "(null)"
1901
+ source: tenantKey ? "resolved" : "none"
1821
1902
  });
1822
1903
  if (!tenantKey) {
1823
1904
  throw new SanitizedError(
1824
1905
  "TENANT_KEY_RESOLUTION_FAILED",
1825
1906
  "Unable to resolve tenant",
1826
- new Error("Tenant key missing")
1907
+ new Error(`Tenant key missing for refresh flow at url=${getUrl().href}`)
1827
1908
  );
1828
1909
  }
1829
1910
  const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
@@ -1852,22 +1933,17 @@ var TenantSwitchFlow = class {
1852
1933
  emitAuthEvent(AuthEventNames.TENANT_SWITCHED, { tenantKey: newTenantKey ?? void 0 });
1853
1934
  removeStorage(getStorageKey("tenant_switched"));
1854
1935
  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();
1936
+ const tenantKey = (newTenantKey || resolveTenantKey())?.trim() || null;
1859
1937
  log.debug("[TenantSwitchFlow] Tenant key resolved", {
1860
1938
  tenantKey: tenantKey ?? "(null)",
1861
1939
  fromNewTenantKey: newTenantKey ?? "(null)",
1862
- fromRuntime: tenantKeyFromRuntime ?? "(null)",
1863
- fromStorage: tenantKeyFromStorage ?? "(null)",
1864
- fromConfig: tenantKeyFromConfig || "(null)"
1940
+ source: newTenantKey ? "new_tenant_key" : tenantKey ? "resolved" : "none"
1865
1941
  });
1866
1942
  if (!tenantKey) {
1867
1943
  throw new SanitizedError(
1868
1944
  "TENANT_KEY_RESOLUTION_FAILED",
1869
1945
  "Unable to resolve tenant after switch",
1870
- new Error("Tenant key missing after switch")
1946
+ new Error(`Tenant key missing after switch at url=${getUrl().href}`)
1871
1947
  );
1872
1948
  }
1873
1949
  const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
@@ -1878,6 +1954,21 @@ var TenantSwitchFlow = class {
1878
1954
  new Error(`Tenant config resolution failed for tenantKey: ${tenantKey}`)
1879
1955
  );
1880
1956
  }
1957
+ try {
1958
+ const cfg = getAuthConfig2();
1959
+ writeHostConfigCache({
1960
+ apiBaseUrl: cfg.apiBaseUrl,
1961
+ authServerUrl: cfg.authServerUrl,
1962
+ callbackUrl: cfg.callbackUrl,
1963
+ appId: cfg.appId,
1964
+ clientCode: tenantConfig.client?.clientCode ?? cfg.clientCode ?? "",
1965
+ clientId: cfg.clientId || tenantConfig.client?.clientId,
1966
+ tenantId: tenantConfig.tenantId ?? cfg.tenantId,
1967
+ tenantKey,
1968
+ tenantCode: tenantKey
1969
+ });
1970
+ } catch {
1971
+ }
1881
1972
  setTenantKey(tenantKey);
1882
1973
  storeTenantConfig(tenantConfig);
1883
1974
  await initiateAuthFlow(tenantConfig);
@@ -1902,21 +1993,16 @@ var LoginFlow = class {
1902
1993
  }
1903
1994
  const wasStateMismatch = getStorage(getStorageKey("state_mismatch")) === "true";
1904
1995
  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();
1996
+ const tenantKey = resolveTenantKey();
1909
1997
  log.debug("[LoginFlow] Tenant key resolved", {
1910
1998
  tenantKey: tenantKey ?? "(null)",
1911
- fromRuntime: tenantKeyFromRuntime ?? "(null)",
1912
- fromStorage: tenantKeyFromStorage ?? "(null)",
1913
- fromConfig: tenantKeyFromConfig || "(null)"
1999
+ source: tenantKey ? "resolved" : "none"
1914
2000
  });
1915
2001
  if (!tenantKey) {
1916
2002
  throw new SanitizedError(
1917
2003
  "TENANT_KEY_RESOLUTION_FAILED",
1918
2004
  "Unable to resolve tenant",
1919
- new Error("Tenant key missing")
2005
+ new Error(`Tenant key missing for login flow at url=${url.href}`)
1920
2006
  );
1921
2007
  }
1922
2008
  log.debug("[LoginFlow] Resolving tenant config", { tenantKey });
@@ -1945,7 +2031,6 @@ var LoginFlow = class {
1945
2031
  };
1946
2032
 
1947
2033
  // src/application/flows/getFlowForState.ts
1948
- var forceLoginFlow = new ForceLoginFlow();
1949
2034
  var callbackFlow = new CallbackFlow();
1950
2035
  var authenticatedFlow = new AuthenticatedFlow();
1951
2036
  var refreshFlow = new RefreshFlow();
@@ -1953,8 +2038,6 @@ var tenantSwitchFlow = new TenantSwitchFlow();
1953
2038
  var loginFlow = new LoginFlow();
1954
2039
  function getFlowForState(state) {
1955
2040
  switch (state) {
1956
- case "FORCE_LOGIN" /* FORCE_LOGIN */:
1957
- return forceLoginFlow;
1958
2041
  case "PROCESSING_CALLBACK" /* PROCESSING_CALLBACK */:
1959
2042
  return callbackFlow;
1960
2043
  case "AUTHENTICATED" /* AUTHENTICATED */:
@@ -2047,6 +2130,22 @@ async function bootstrap() {
2047
2130
  return bootstrapImpl();
2048
2131
  }
2049
2132
 
2133
+ // src/infrastructure/watchers/watcher-guard.ts
2134
+ var suspended = false;
2135
+ var generation = 0;
2136
+ function suspendWatchers() {
2137
+ suspended = true;
2138
+ generation += 1;
2139
+ return generation;
2140
+ }
2141
+ function resumeWatchers(expectedGen) {
2142
+ if (expectedGen !== void 0 && expectedGen !== generation) return;
2143
+ suspended = false;
2144
+ }
2145
+ function areWatchersSuspended() {
2146
+ return suspended;
2147
+ }
2148
+
2050
2149
  // src/strategies/cookie/cookie-auth.ts
2051
2150
  var COOKIE_NAMES = {
2052
2151
  accessToken: "tenneo_auth_access_token",
@@ -2173,36 +2272,14 @@ function checkCookiesDeleted() {
2173
2272
  }
2174
2273
  }
2175
2274
  function handleCookieDeletion() {
2275
+ if (areWatchersSuspended()) {
2276
+ logger.debug("Cookie deletion handler skipped \u2014 watchers suspended (logout/teardown)");
2277
+ return;
2278
+ }
2176
2279
  try {
2177
2280
  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
2281
  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
- }
2282
+ clearAuthSessionStorage();
2206
2283
  const cb = getSessionClearedCallback();
2207
2284
  if (cb) {
2208
2285
  Promise.resolve(cb()).catch((error) => {
@@ -2236,13 +2313,7 @@ function startCookieWatcher() {
2236
2313
  allCookies: initialCookies
2237
2314
  });
2238
2315
  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
- }
2316
+ if (areWatchersSuspended()) return;
2246
2317
  const cookiesDeleted = checkCookiesDeleted();
2247
2318
  if (cookiesDeleted) {
2248
2319
  logger.info("Cookies deleted detected by cookie watcher");
@@ -2306,9 +2377,11 @@ function checkStorageCleared() {
2306
2377
  }
2307
2378
  }
2308
2379
  function handleStorageEvent(event) {
2380
+ if (areWatchersSuspended()) return;
2309
2381
  if (typeof window !== "undefined" && event.storageArea === window.sessionStorage) {
2310
2382
  const wasCleared = checkStorageCleared();
2311
2383
  if (wasCleared) {
2384
+ if (areWatchersSuspended()) return;
2312
2385
  logger.info("Detected manual sessionStorage clear! Redirecting to login...");
2313
2386
  const cb = getSessionClearedCallback();
2314
2387
  if (cb) {
@@ -2328,13 +2401,10 @@ function startStorageWatcher() {
2328
2401
  storageEventListener = handleStorageEvent;
2329
2402
  window.addEventListener("storage", storageEventListener);
2330
2403
  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
- }
2404
+ if (areWatchersSuspended()) return;
2336
2405
  const wasCleared = checkStorageCleared();
2337
2406
  if (wasCleared) {
2407
+ if (areWatchersSuspended()) return;
2338
2408
  logger.info("Detected manual sessionStorage clear (polling)! Redirecting to login...");
2339
2409
  clearInterval(pollInterval);
2340
2410
  const cb = getSessionClearedCallback();
@@ -2419,62 +2489,68 @@ function getCsrfToken() {
2419
2489
  }
2420
2490
  async function callServerLogout(logoutUrl) {
2421
2491
  const csrfToken = getCsrfToken();
2422
- async function doRequest(method) {
2423
- const isPost = method === "POST";
2424
- const headers = {
2425
- "X-Requested-With": "XMLHttpRequest",
2426
- Accept: "application/json, */*"
2427
- };
2428
- if (isPost) {
2429
- headers["Content-Type"] = "application/x-www-form-urlencoded";
2430
- }
2431
- if (csrfToken) {
2432
- headers["X-CSRF-Token"] = csrfToken;
2433
- }
2492
+ const headers = {
2493
+ "X-Requested-With": "XMLHttpRequest",
2494
+ Accept: "application/json, */*",
2495
+ "Content-Type": "application/x-www-form-urlencoded"
2496
+ };
2497
+ if (csrfToken) {
2498
+ headers["X-CSRF-Token"] = csrfToken;
2499
+ }
2500
+ try {
2434
2501
  const response = await fetch(logoutUrl, {
2435
- method,
2502
+ method: "POST",
2436
2503
  mode: "cors",
2437
2504
  headers,
2438
- credentials: "include"
2505
+ credentials: "include",
2506
+ body: ""
2439
2507
  });
2440
2508
  if (!response.ok) {
2441
- logger.warn("Server logout HTTP error", {
2442
- method,
2509
+ logger.warn("[logout] Server logout HTTP error", {
2443
2510
  status: response.status,
2444
2511
  statusText: response.statusText
2445
2512
  });
2446
2513
  return { success: false };
2447
2514
  }
2448
- const data = await response.json().catch(() => ({}));
2449
- logger.info(`Server logout response (${method})`, { response: data });
2515
+ const contentType = response.headers.get("content-type") || "";
2516
+ if (!contentType.includes("application/json")) {
2517
+ logger.info("[logout] Server logout: OK without JSON body", {
2518
+ status: response.status
2519
+ });
2520
+ return { success: true, response: { success: true } };
2521
+ }
2522
+ const text = await response.text();
2523
+ let data = {};
2524
+ if (text?.trim()) {
2525
+ try {
2526
+ data = JSON.parse(text);
2527
+ } catch {
2528
+ logger.warn("[logout] Server returned non-JSON body with JSON content-type");
2529
+ return { success: false };
2530
+ }
2531
+ }
2532
+ logger.info("[logout] Server logout response", { response: data });
2450
2533
  if (data && (data.success === true || data.logout === true)) {
2451
2534
  return { success: true, response: data };
2452
2535
  }
2453
- logger.warn("Server logout response invalid payload", {
2454
- method,
2455
- response: data
2456
- });
2536
+ if (!text || text.trim() === "") {
2537
+ return { success: true, response: { success: true } };
2538
+ }
2539
+ logger.warn("[logout] Server logout: unexpected JSON payload", { response: data });
2457
2540
  return { success: false, response: data };
2458
- }
2459
- try {
2460
- const postResult = await doRequest("POST");
2461
- if (postResult.success) return postResult;
2462
- logger.debug("POST logout did not succeed, trying GET");
2463
- const getResult = await doRequest("GET");
2464
- return getResult;
2465
2541
  } catch (error) {
2466
- logger.warn("Server logout failed (both POST and GET)", {
2542
+ logger.warn("[logout] Server logout request failed", {
2467
2543
  error: error instanceof Error ? error.message : String(error)
2468
2544
  });
2469
2545
  return { success: false };
2470
2546
  }
2471
2547
  }
2472
2548
  function performLocalLogoutCleanup() {
2473
- logger.debug("Performing local logout cleanup");
2549
+ logger.debug("[logout] Local cleanup: cookies + auth storage");
2474
2550
  try {
2475
2551
  getAuthRuntime()?.clearCookies?.clearCookies();
2476
2552
  } catch (error) {
2477
- logger.debug("Runtime cookie clearer failed (ignored)", {
2553
+ logger.debug("[logout] Runtime cookie clearer failed (ignored)", {
2478
2554
  error: error instanceof Error ? error.message : String(error)
2479
2555
  });
2480
2556
  }
@@ -2482,11 +2558,11 @@ function performLocalLogoutCleanup() {
2482
2558
  try {
2483
2559
  clearAuthStorage();
2484
2560
  } catch (error) {
2485
- logger.warn("clearAuthStorage failed (ignored)", {
2561
+ logger.warn("[logout] clearAuthStorage failed", {
2486
2562
  error: error instanceof Error ? error.message : String(error)
2487
2563
  });
2488
2564
  }
2489
- logger.info("Local logout cleanup completed");
2565
+ logger.info("[logout] Local cleanup completed");
2490
2566
  }
2491
2567
  function acquireLogoutLock() {
2492
2568
  const LOCK_KEY = getStorageKey("logout_lock");
@@ -2495,7 +2571,7 @@ function acquireLogoutLock() {
2495
2571
  if (existingLock) {
2496
2572
  const lockTime = Number(existingLock);
2497
2573
  if (!Number.isNaN(lockTime) && now - lockTime < 5e3) {
2498
- logger.debug("Logout lock active \u2013 skipping duplicate logout", {
2574
+ logger.debug("[logout] Lock active \u2014 duplicate tab logout skipped", {
2499
2575
  now,
2500
2576
  lockTime
2501
2577
  });
@@ -2511,102 +2587,95 @@ function releaseLogoutLock() {
2511
2587
  } catch {
2512
2588
  }
2513
2589
  }
2514
- async function logout(tenantKey) {
2515
- setStorage(getStorageKey("force_login"), "true");
2516
- if (tenantKey?.trim()) {
2517
- setStorage(getStorageKey("preserved_tenant_key"), tenantKey.trim());
2518
- }
2590
+ var logoutInFlight = null;
2591
+ async function runLogoutPipeline(tenantKey) {
2592
+ const gen = suspendWatchers();
2593
+ logger.info("[logout] Lifecycle: suspend watchers", { generation: gen });
2594
+ stopStorageWatcher();
2595
+ stopCookieWatcher();
2519
2596
  if (!acquireLogoutLock()) {
2520
- return { success: true, message: "Logout already in progress." };
2597
+ resumeWatchers(gen);
2598
+ logger.debug("[logout] Lifecycle: resume (skipped \u2014 lock)");
2599
+ return { success: true, message: "Logout already in progress in another tab." };
2521
2600
  }
2601
+ let serverLogoutSuccess = false;
2602
+ let serverResponse;
2522
2603
  try {
2523
- stopStorageWatcher();
2524
- stopCookieWatcher();
2525
- const apiAuthBaseUrl = Config.getAuthServerUrl();
2526
- let serverLogoutSuccess = false;
2527
- let serverResponse;
2604
+ if (tenantKey?.trim()) {
2605
+ applyRuntimeTenantSwitch(tenantKey.trim(), { syncStorageCache: true });
2606
+ logger.info("[logout] Runtime tenant applied for logout context", {
2607
+ tenantKey: tenantKey.trim()
2608
+ });
2609
+ }
2610
+ const authConfig = getAuthConfig2();
2611
+ const apiAuthBaseUrl = authConfig.authServerUrl;
2528
2612
  if (apiAuthBaseUrl) {
2529
2613
  const logoutUrl = buildUrl(apiAuthBaseUrl, "account/logout");
2530
- logger.info("Calling auth server logout", { logoutUrl });
2614
+ logger.info("[logout] Calling auth server logout (POST)", { logoutUrl });
2531
2615
  const logoutResult = await callServerLogout(logoutUrl);
2532
2616
  serverLogoutSuccess = logoutResult.success;
2533
2617
  serverResponse = logoutResult.response;
2534
2618
  if (!serverLogoutSuccess) {
2535
- logger.warn("Server logout failed \u2013 session may still exist", {
2619
+ logger.warn("[logout] Server logout did not confirm success \u2014 clearing local session anyway", {
2536
2620
  logoutUrl,
2537
2621
  serverResponse
2538
2622
  });
2539
2623
  }
2540
2624
  } else {
2541
- logger.warn("API auth base URL not configured, performing local logout only");
2625
+ logger.warn("[logout] authServerUrl missing \u2014 local logout only");
2542
2626
  }
2627
+ } catch (error) {
2628
+ logger.error("[logout] Unexpected error before local cleanup", {
2629
+ error: error instanceof Error ? error.message : String(error)
2630
+ });
2631
+ }
2632
+ try {
2543
2633
  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
- }
2563
- resetCookieWatcher();
2564
- startStorageWatcher();
2565
- releaseLogoutLock();
2566
- logger.info("Logout completed \u2013 flags set, storage cleared, fresh login required", {
2567
- serverLogoutSuccess,
2568
- serverResponse
2634
+ } catch (error) {
2635
+ logger.error("[logout] Local cleanup threw", {
2636
+ error: error instanceof Error ? error.message : String(error)
2569
2637
  });
2570
- emitAuthEvent(AuthEventNames.LOGOUT_COMPLETED, {
2571
- serverLogoutSuccess,
2572
- tenantKey: getStorage(getStorageKey("preserved_tenant_key")) || void 0,
2573
- message: serverLogoutSuccess ? "Server logout success" : "Local logout only"
2638
+ }
2639
+ try {
2640
+ syncHostConfigCacheFromMemory();
2641
+ logger.debug("[logout] host_config cache refreshed from runtime (not restored from snapshot)");
2642
+ } catch (error) {
2643
+ logger.warn("[logout] Failed to sync host config cache", {
2644
+ error: error instanceof Error ? error.message : String(error)
2574
2645
  });
2575
- return {
2576
- success: true,
2577
- message: serverLogoutSuccess ? serverResponse?.message || "Logout successful. Session cookies cleared. Fresh login required." : "Logged out locally. Fresh login required (server logout may have failed).",
2578
- serverResponse
2579
- };
2646
+ }
2647
+ try {
2648
+ resetCookieWatcher();
2649
+ startStorageWatcher();
2650
+ logger.info("[logout] Watchers restarted");
2580
2651
  } catch (error) {
2581
- logger.error("Logout failed unexpectedly", {
2652
+ logger.warn("[logout] Watcher restart failed", {
2582
2653
  error: error instanceof Error ? error.message : String(error)
2583
2654
  });
2584
- 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
- }
2604
- releaseLogoutLock();
2605
- return {
2606
- success: true,
2607
- message: "Logged out locally due to an error. Fresh login required."
2608
- };
2609
2655
  }
2656
+ releaseLogoutLock();
2657
+ resumeWatchers(gen);
2658
+ logger.info("[logout] Lifecycle: complete", { serverLogoutSuccess });
2659
+ emitAuthEvent(AuthEventNames.LOGOUT_COMPLETED, {
2660
+ serverLogoutSuccess,
2661
+ tenantKey: tenantKey?.trim() || getAuthConfig2().tenantCode || void 0,
2662
+ message: serverLogoutSuccess ? "Server logout success" : "Local logout (server may be pending)"
2663
+ });
2664
+ return {
2665
+ success: true,
2666
+ message: serverLogoutSuccess ? serverResponse?.message || "Logout successful. Fresh login required." : "Logged out locally. Fresh login required (server logout may have failed).",
2667
+ serverResponse
2668
+ };
2669
+ }
2670
+ async function logout(tenantKey) {
2671
+ if (logoutInFlight) {
2672
+ logger.debug("[logout] Coalescing with in-flight logout");
2673
+ return logoutInFlight;
2674
+ }
2675
+ logoutInFlight = runLogoutPipeline(tenantKey).finally(() => {
2676
+ logoutInFlight = null;
2677
+ });
2678
+ return logoutInFlight;
2610
2679
  }
2611
2680
  var AuthContext = react.createContext(void 0);
2612
2681
  function useAuthContext() {
@@ -2681,7 +2750,6 @@ async function resolveTenant(tenantKey) {
2681
2750
  scopes: DEFAULT_SCOPES2
2682
2751
  }
2683
2752
  };
2684
- console.log("config====", config);
2685
2753
  logger.debug("Resolved tenant config", { tenantCode });
2686
2754
  return config;
2687
2755
  } catch (error) {
@@ -2942,27 +3010,29 @@ function AuthProvider({
2942
3010
  logger.warn("[AuthProvider] callbackUrl missing from host props");
2943
3011
  }
2944
3012
  setConfig({
2945
- ...mergedTenantCode ? { tenantCode: mergedTenantCode } : {},
3013
+ ...mergedTenantCode ? { tenantCode: mergedTenantCode, tenantKey: mergedTenantCode } : {},
2946
3014
  ...mergedApiBaseUrl ? { apiBaseUrl: mergedApiBaseUrl } : {},
2947
3015
  ...mergedAuthServerUrl ? { authServerUrl: mergedAuthServerUrl } : {},
2948
3016
  ...mergedCallbackUrl ? { callbackUrl: mergedCallbackUrl } : {}
2949
3017
  });
3018
+ logConfigResolutionDebug("AuthProvider host props applied");
2950
3019
  return () => {
2951
3020
  clearConfigOverrides();
2952
3021
  };
2953
3022
  }, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
2954
3023
  react.useEffect(() => {
2955
3024
  try {
2956
- const payload = {
3025
+ writeHostConfigCache({
2957
3026
  tenantCode: mergedTenantCode ?? "",
3027
+ tenantKey: mergedTenantCode ?? "",
3028
+ appId: appId ?? "",
2958
3029
  apiBaseUrl: mergedApiBaseUrl ?? "",
2959
3030
  authServerUrl: mergedAuthServerUrl ?? "",
2960
3031
  callbackUrl: mergedCallbackUrl ?? ""
2961
- };
2962
- setStorage(getStorageKey("host_config"), JSON.stringify(payload));
3032
+ });
2963
3033
  } catch {
2964
3034
  }
2965
- }, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
3035
+ }, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl, appId]);
2966
3036
  react.useLayoutEffect(() => {
2967
3037
  setStorageKeyPrefix(resolvedTenantKey ?? "");
2968
3038
  const adapter = storageAdapter !== void 0 && storageAdapter !== null ? storageAdapter : createSecureSessionStorageAdapter(namespace);
@@ -3003,24 +3073,18 @@ function AuthProvider({
3003
3073
  const currentUrl = window.location.href;
3004
3074
  const isCallback = currentUrl.includes("code=") || currentUrl.includes("state=");
3005
3075
  if (isCallback) {
3006
- const cleanUrl2 = window.location.origin + window.location.pathname;
3007
- window.history.replaceState({}, document.title, cleanUrl2);
3076
+ const cleanUrl = window.location.origin + window.location.pathname;
3077
+ window.history.replaceState({}, document.title, cleanUrl);
3008
3078
  }
3009
3079
  }
3010
3080
  removeStorage(getStorageKey("access_token"));
3011
3081
  removeStorage(getStorageKey("refresh_token"));
3012
3082
  removeStorage(getStorageKey("expires_at"));
3013
- removeStorage(getStorageKey("id_token"));
3014
3083
  removeStorage(getStorageKey("state"));
3015
3084
  removeStorage(getStorageKey("code_verifier"));
3016
3085
  removeStorage(getStorageKey("callback_processed"));
3017
3086
  clearCallbackProcessing();
3018
3087
  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
3088
  setStorage(getStorageKey("props_changed"), "true");
3025
3089
  setStorage(getStorageKey("tenant_switched"), "true");
3026
3090
  setStorage(getStorageKey("new_tenant_key"), resolvedTenantKey2);
@@ -3033,7 +3097,17 @@ function AuthProvider({
3033
3097
  } else if (previousTenantKey) {
3034
3098
  logger.debug("[AuthProvider] TenantKey unchanged", { tenantKey: resolvedTenantKey2 });
3035
3099
  }
3036
- setStorage(getStorageKey("props_config"), JSON.stringify({ tenantKey: resolvedTenantKey2 }));
3100
+ try {
3101
+ writeHostConfigCache({
3102
+ tenantKey: resolvedTenantKey2,
3103
+ tenantCode: resolvedTenantKey2
3104
+ });
3105
+ logConfigResolutionDebug("AuthProvider tenant key synced to cache", {
3106
+ tenantCode: resolvedTenantKey2,
3107
+ tenantKey: resolvedTenantKey2
3108
+ });
3109
+ } catch {
3110
+ }
3037
3111
  setStorage(getStorageKey("tenant_key"), resolvedTenantKey2);
3038
3112
  } else {
3039
3113
  logger.debug("[AuthProvider] No tenant code determined");
@@ -3232,17 +3306,29 @@ function AuthCallback({
3232
3306
  setStatus("loading");
3233
3307
  setMessage("Completing sign-in...");
3234
3308
  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 {
3309
+ if (!handled) {
3242
3310
  setStatus("error");
3243
3311
  setMessage("Authentication failed. Please try again.");
3244
3312
  window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
3313
+ return;
3314
+ }
3315
+ const startedAt = Date.now();
3316
+ const MAX_WAIT_MS = 5e3;
3317
+ while (true) {
3318
+ const token = getAccessToken();
3319
+ if (token) {
3320
+ setTokens(token);
3321
+ setStatus("success");
3322
+ setMessage("Authentication successful. Redirecting...");
3323
+ window.location.replace(successPath);
3324
+ return;
3325
+ }
3326
+ if (Date.now() - startedAt > MAX_WAIT_MS) break;
3327
+ await new Promise((r) => setTimeout(r, 200));
3245
3328
  }
3329
+ setStatus("error");
3330
+ setMessage("Authentication did not complete. Please try again.");
3331
+ window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
3246
3332
  };
3247
3333
  runCallback();
3248
3334
  }, [homePath, loginPath, successPath, setTokens]);
@@ -3424,6 +3510,7 @@ function useAuthInit() {
3424
3510
  }
3425
3511
 
3426
3512
  exports.AuthCallback = AuthCallback;
3513
+ exports.AuthConfigError = AuthConfigError;
3427
3514
  exports.AuthEventNames = AuthEventNames;
3428
3515
  exports.AuthProvider = AuthProvider;
3429
3516
  exports.Config = Config;
@@ -3432,6 +3519,8 @@ exports.ProtectedRoute = ProtectedRoute;
3432
3519
  exports.STORAGE_KEYS = STORAGE_KEYS2;
3433
3520
  exports.STORAGE_KEY_PREFIX = STORAGE_KEY_PREFIX;
3434
3521
  exports.SanitizedError = SanitizedError;
3522
+ exports.applyRuntimeTenantSwitch = applyRuntimeTenantSwitch;
3523
+ exports.areWatchersSuspended = areWatchersSuspended;
3435
3524
  exports.bootstrap = bootstrap;
3436
3525
  exports.buildStorageKey = buildStorageKey;
3437
3526
  exports.createAuthEngine = createAuthEngine;
@@ -3444,27 +3533,41 @@ exports.enforceHttps = enforceHttps;
3444
3533
  exports.ensureValidAccessToken = ensureValidAccessToken;
3445
3534
  exports.extractTenantKey = extractTenantKey;
3446
3535
  exports.getAccessToken = getAccessToken2;
3536
+ exports.getAuthConfig = getAuthConfig2;
3447
3537
  exports.getAuthRuntime = getAuthRuntime;
3448
3538
  exports.getConfig = getConfig;
3539
+ exports.getConfigFieldSources = getConfigFieldSources;
3540
+ exports.getEffectiveTenantCode = getEffectiveTenantCode;
3541
+ exports.getEffectiveTenantKey = getEffectiveTenantKey;
3449
3542
  exports.getStorageAdapter = getStorageAdapter;
3450
3543
  exports.getStorageNamespace = getStorageNamespace;
3451
3544
  exports.getStoredAccessToken = getAccessToken;
3545
+ exports.initPlugin = initPlugin;
3452
3546
  exports.isAccessTokenExpired = isAccessTokenExpired;
3453
3547
  exports.isCallbackUrl = isCallbackUrl;
3548
+ exports.logConfigResolutionDebug = logConfigResolutionDebug;
3454
3549
  exports.logout = logout;
3455
3550
  exports.refreshAccessToken = refreshAccessToken;
3551
+ exports.requireAuthConfig = requireAuthConfig;
3456
3552
  exports.resetCookieWatcher = resetCookieWatcher;
3457
3553
  exports.resolveConfig = resolveConfig;
3458
3554
  exports.resolveTenant = resolveTenant;
3555
+ exports.resumeWatchers = resumeWatchers;
3459
3556
  exports.sanitizeErrorMessage = sanitizeErrorMessage;
3460
3557
  exports.setAuthRuntime = setAuthRuntime;
3461
3558
  exports.setConfig = setConfig;
3559
+ exports.setHostConfigProvider = setHostConfigProvider;
3462
3560
  exports.setStorageContext = setStorageContext;
3463
3561
  exports.startCookieWatcher = startCookieWatcher;
3562
+ exports.startStorageWatcher = startStorageWatcher;
3464
3563
  exports.stopCookieWatcher = stopCookieWatcher;
3564
+ exports.stopStorageWatcher = stopStorageWatcher;
3465
3565
  exports.subscribeAuthEvent = subscribeAuthEvent;
3566
+ exports.suspendWatchers = suspendWatchers;
3567
+ exports.syncHostConfigCacheFromMemory = syncHostConfigCacheFromMemory;
3466
3568
  exports.useAuth = useAuth;
3467
3569
  exports.useAuthContext = useAuthContext;
3468
3570
  exports.useAuthInit = useAuthInit;
3571
+ exports.writeHostConfigCache = writeHostConfigCache;
3469
3572
  //# sourceMappingURL=index.js.map
3470
3573
  //# sourceMappingURL=index.js.map