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.mjs CHANGED
@@ -15,64 +15,6 @@ function getAuthRuntime() {
15
15
  function isLocalhost(hostname) {
16
16
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.endsWith(".local");
17
17
  }
18
- function extractTenantKeyFromDomain(urlOrHostname, config) {
19
- try {
20
- let hostname;
21
- if (urlOrHostname.startsWith("http://") || urlOrHostname.startsWith("https://")) {
22
- try {
23
- const url = new URL(urlOrHostname);
24
- hostname = url.hostname;
25
- } catch {
26
- hostname = urlOrHostname.replace(/^https?:\/\//, "").split("/")[0];
27
- }
28
- } else {
29
- hostname = urlOrHostname;
30
- }
31
- const parts = hostname.split(".");
32
- if (parts.length < 2) return config.getClientCode();
33
- const tenantKey = parts[0];
34
- if (!tenantKey || tenantKey.trim() === "") return config.getClientCode();
35
- if (tenantKey.trim().toLowerCase() === "productlms") return config.getClientCode();
36
- return tenantKey.trim();
37
- } catch {
38
- return config.getClientCode();
39
- }
40
- }
41
- function extractTenantKeyFromUrl(url, config, getStorage3, getKey) {
42
- try {
43
- const propsConfigKey = getKey ? getKey("props_config") : "tenneo_auth_props_config";
44
- const propsConfigStr = getStorage3(propsConfigKey);
45
- if (propsConfigStr) {
46
- try {
47
- const propsConfig = JSON.parse(propsConfigStr);
48
- if (propsConfig.tenantKey?.trim()) return propsConfig.tenantKey.trim();
49
- } catch {
50
- }
51
- }
52
- const tenantKeyStorageKey = getKey ? getKey("tenant_key") : "tenneo_auth_tenant_key";
53
- const storedTenantKey = getStorage3(tenantKeyStorageKey);
54
- if (storedTenantKey?.trim()) return storedTenantKey.trim();
55
- const urlParams = new URLSearchParams(url.search);
56
- const tenantKeyFromQuery = urlParams.get("t");
57
- if (tenantKeyFromQuery?.trim()) {
58
- const hostnameParts = url.hostname.split(".");
59
- const baseDomain = hostnameParts.length >= 2 ? hostnameParts.slice(-2).join(".") : url.hostname;
60
- const simulatedHostname = `${tenantKeyFromQuery.trim()}.${baseDomain}`;
61
- const key = extractTenantKeyFromDomain(simulatedHostname, config);
62
- if (key) return key;
63
- return tenantKeyFromQuery.trim();
64
- }
65
- const keyFromHost = extractTenantKeyFromDomain(url.hostname, config);
66
- if (keyFromHost) return keyFromHost;
67
- const clientCode = config.getClientCode();
68
- if (clientCode?.trim()) return clientCode.trim();
69
- return config.getClientCode();
70
- } catch {
71
- const clientCode = config.getClientCode();
72
- if (clientCode?.trim()) return clientCode.trim();
73
- return config.getClientCode();
74
- }
75
- }
76
18
  function isCallbackUrlFromUrl(url) {
77
19
  const params = new URLSearchParams(url.search);
78
20
  return params.has("code") && params.has("state");
@@ -133,8 +75,7 @@ var CONFIG_KEY_SUFFIXES = {
133
75
  stateMismatch: "state_mismatch",
134
76
  bootstrapInProgress: "bootstrap_in_progress",
135
77
  bootstrapTimestamp: "bootstrap_timestamp",
136
- callbackHandled: "callback_handled",
137
- idToken: "id_token"
78
+ callbackHandled: "callback_handled"
138
79
  };
139
80
 
140
81
  // src/infrastructure/storage/context.ts
@@ -426,51 +367,7 @@ function createCookieStorageAdapter(namespace, options) {
426
367
  };
427
368
  }
428
369
 
429
- // src/infrastructure/storage/api.ts
430
- var KEY_SUFFIXES = {
431
- authServerUrl: "auth_server_url",
432
- clientId: "client_id",
433
- redirectUri: "redirect_uri",
434
- tenantId: "tenant_id",
435
- tenantKey: "tenant_key",
436
- codeVerifier: "code_verifier",
437
- state: "state",
438
- accessToken: "access_token",
439
- refreshToken: "refresh_token",
440
- expiresAt: "expires_at"
441
- };
442
- var STORAGE_KEYS = {
443
- get authServerUrl() {
444
- return getStorageKey(KEY_SUFFIXES.authServerUrl);
445
- },
446
- get clientId() {
447
- return getStorageKey(KEY_SUFFIXES.clientId);
448
- },
449
- get redirectUri() {
450
- return getStorageKey(KEY_SUFFIXES.redirectUri);
451
- },
452
- get tenantId() {
453
- return getStorageKey(KEY_SUFFIXES.tenantId);
454
- },
455
- get tenantKey() {
456
- return getStorageKey(KEY_SUFFIXES.tenantKey);
457
- },
458
- get codeVerifier() {
459
- return getStorageKey(KEY_SUFFIXES.codeVerifier);
460
- },
461
- get state() {
462
- return getStorageKey(KEY_SUFFIXES.state);
463
- },
464
- get accessToken() {
465
- return getStorageKey(KEY_SUFFIXES.accessToken);
466
- },
467
- get refreshToken() {
468
- return getStorageKey(KEY_SUFFIXES.refreshToken);
469
- },
470
- get expiresAt() {
471
- return getStorageKey(KEY_SUFFIXES.expiresAt);
472
- }
473
- };
370
+ // src/infrastructure/storage/kv.ts
474
371
  function keyToSuffix(key) {
475
372
  const prefix = getStorageKeyPrefix();
476
373
  return key.startsWith(prefix) ? key.slice(prefix.length) : key;
@@ -506,23 +403,53 @@ function removeStorage(key) {
506
403
  } catch {
507
404
  }
508
405
  }
509
- function clearAuthStorage() {
510
- const adapter = getStorageAdapter();
511
- if (!adapter?.clearSync) return;
512
- try {
513
- adapter.clearTokensSync?.();
514
- adapter.clearSync();
515
- } catch {
406
+
407
+ // src/infrastructure/storage/api.ts
408
+ var KEY_SUFFIXES = {
409
+ tenantKey: "tenant_key",
410
+ codeVerifier: "code_verifier",
411
+ state: "state",
412
+ accessToken: "access_token",
413
+ refreshToken: "refresh_token",
414
+ expiresAt: "expires_at"
415
+ };
416
+ var STORAGE_KEYS = {
417
+ get tenantKey() {
418
+ return getStorageKey(KEY_SUFFIXES.tenantKey);
419
+ },
420
+ get codeVerifier() {
421
+ return getStorageKey(KEY_SUFFIXES.codeVerifier);
422
+ },
423
+ get state() {
424
+ return getStorageKey(KEY_SUFFIXES.state);
425
+ },
426
+ get accessToken() {
427
+ return getStorageKey(KEY_SUFFIXES.accessToken);
428
+ },
429
+ get refreshToken() {
430
+ return getStorageKey(KEY_SUFFIXES.refreshToken);
431
+ },
432
+ get expiresAt() {
433
+ return getStorageKey(KEY_SUFFIXES.expiresAt);
516
434
  }
435
+ };
436
+ function clearAuthStorage() {
437
+ const keysToClear = [
438
+ STORAGE_KEYS.accessToken,
439
+ STORAGE_KEYS.refreshToken,
440
+ STORAGE_KEYS.expiresAt,
441
+ STORAGE_KEYS.codeVerifier,
442
+ STORAGE_KEYS.state,
443
+ getStorageKey("callback_processed"),
444
+ getStorageKey("props_config"),
445
+ getStorageKey("tenant_switched"),
446
+ getStorageKey("new_tenant_key"),
447
+ getStorageKey("preserved_tenant_key")
448
+ ];
449
+ for (const key of keysToClear) removeStorage(key);
517
450
  }
518
451
  function clearAuthSessionStorage() {
519
- const adapter = getStorageAdapter();
520
- if (!adapter?.clearSync) return;
521
- try {
522
- adapter.clearTokensSync?.();
523
- adapter.clearSync();
524
- } catch {
525
- }
452
+ clearAuthStorage();
526
453
  }
527
454
  function clearAllCookies() {
528
455
  const runtime = getAuthRuntime();
@@ -531,27 +458,8 @@ function clearAllCookies() {
531
458
  } catch {
532
459
  }
533
460
  }
534
- function clearAllBrowserStorage() {
535
- if (typeof window === "undefined") return;
536
- try {
537
- window.localStorage?.clear();
538
- } catch {
539
- }
540
- try {
541
- window.sessionStorage?.clear();
542
- } catch {
543
- }
544
- }
545
461
  function clearAllStorage() {
546
- const forceLogin = getStorage(getStorageKey("force_login"));
547
- const logoutInProgress = getStorage(getStorageKey("logout_in_progress"));
548
- const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
549
- clearAuthSessionStorage();
550
462
  clearAuthStorage();
551
- clearAllBrowserStorage();
552
- if (forceLogin === "true") setStorage(getStorageKey("force_login"), "true");
553
- if (logoutInProgress === "true") setStorage(getStorageKey("logout_in_progress"), "true");
554
- if (preservedTenantKey) setStorage(getStorageKey("preserved_tenant_key"), preservedTenantKey);
555
463
  clearAllCookies();
556
464
  }
557
465
  function getAccessToken() {
@@ -587,15 +495,6 @@ function isAccessTokenExpired() {
587
495
  const buffer = 60 * 1e3;
588
496
  return Date.now() >= expiresAt - buffer;
589
497
  }
590
- function getClientId() {
591
- return getStorage(STORAGE_KEYS.clientId);
592
- }
593
- function getRedirectUri() {
594
- return getStorage(STORAGE_KEYS.redirectUri);
595
- }
596
- function getTenantId() {
597
- return getStorage(STORAGE_KEYS.tenantId);
598
- }
599
498
  function getCodeVerifier() {
600
499
  return getStorage(STORAGE_KEYS.codeVerifier);
601
500
  }
@@ -656,6 +555,252 @@ var Logger = class {
656
555
  };
657
556
  var logger = new Logger();
658
557
 
558
+ // src/config/resolver-core.ts
559
+ var inMemoryConfig = {};
560
+ var hostConfigProvider = null;
561
+ var CACHE_MARKER = "__tenneo_host_config_cache";
562
+ var CACHE_AT = "__tenneo_host_config_cache_at";
563
+ function setConfig(overrides) {
564
+ inMemoryConfig = { ...inMemoryConfig, ...overrides };
565
+ }
566
+ function clearConfigOverrides() {
567
+ inMemoryConfig = {};
568
+ }
569
+ function setHostConfigProvider(fn) {
570
+ hostConfigProvider = fn;
571
+ }
572
+ function readHostBridge() {
573
+ const out = {};
574
+ try {
575
+ if (typeof window !== "undefined" && window.__TENNEO_AUTH_HOST__) {
576
+ Object.assign(out, window.__TENNEO_AUTH_HOST__);
577
+ }
578
+ } catch {
579
+ }
580
+ try {
581
+ const fromProvider = hostConfigProvider?.();
582
+ if (fromProvider && typeof fromProvider === "object") {
583
+ Object.assign(out, fromProvider);
584
+ }
585
+ } catch (e) {
586
+ logger.warn("[config] hostConfigProvider threw", {
587
+ error: e instanceof Error ? e.message : String(e)
588
+ });
589
+ }
590
+ return out;
591
+ }
592
+ function readStorageConfigCache() {
593
+ try {
594
+ const raw = getStorage(getStorageKey("host_config"));
595
+ if (!raw) return {};
596
+ const parsed = JSON.parse(raw);
597
+ if (!parsed || typeof parsed !== "object") return {};
598
+ const { [CACHE_MARKER]: _m, [CACHE_AT]: _t, ...rest } = parsed;
599
+ if (!rest.clientId && typeof rest.client_id === "string" && rest.client_id.trim()) {
600
+ rest.clientId = rest.client_id.trim();
601
+ }
602
+ return rest;
603
+ } catch {
604
+ return {};
605
+ }
606
+ }
607
+ function resolveSDKConfig(overrides) {
608
+ const stored = readStorageConfigCache();
609
+ const bridge = readHostBridge();
610
+ return {
611
+ ...stored,
612
+ ...bridge,
613
+ ...inMemoryConfig,
614
+ ...overrides ?? {}
615
+ };
616
+ }
617
+ function nonEmptyString(v) {
618
+ if (v == null) return void 0;
619
+ const s = String(v).trim();
620
+ return s || void 0;
621
+ }
622
+ function getConfigFieldSources(overrides) {
623
+ const stored = readStorageConfigCache();
624
+ const bridge = readHostBridge();
625
+ const layers = [
626
+ { src: "storage_cache", cfg: stored },
627
+ { src: "host_bridge", cfg: bridge },
628
+ { src: "memory", cfg: inMemoryConfig }
629
+ ];
630
+ if (overrides && Object.keys(overrides).length) {
631
+ layers.push({ src: "override", cfg: overrides });
632
+ }
633
+ const pick = (key) => {
634
+ for (let i = layers.length - 1; i >= 0; i--) {
635
+ const v = nonEmptyString(layers[i].cfg[key]);
636
+ if (v !== void 0) return layers[i].src;
637
+ }
638
+ return void 0;
639
+ };
640
+ return {
641
+ tenantCode: pick("tenantCode") ?? pick("tenantKey"),
642
+ tenantKey: pick("tenantKey") ?? pick("tenantCode"),
643
+ apiBaseUrl: pick("apiBaseUrl"),
644
+ authServerUrl: pick("authServerUrl"),
645
+ callbackUrl: pick("callbackUrl"),
646
+ clientCode: pick("clientCode"),
647
+ clientId: pick("clientId"),
648
+ tenantId: pick("tenantId"),
649
+ appId: pick("appId")
650
+ };
651
+ }
652
+ function getEffectiveTenantCode(overrides) {
653
+ const cfg = resolveSDKConfig(overrides);
654
+ const t = nonEmptyString(cfg.tenantCode) ?? nonEmptyString(cfg.tenantKey) ?? nonEmptyString(cfg.clientCode) ?? "";
655
+ return t;
656
+ }
657
+ var getEffectiveTenantKey = getEffectiveTenantCode;
658
+ function trim(v) {
659
+ return v == null ? "" : String(v).trim();
660
+ }
661
+ function toResolvedConfig(cfg) {
662
+ const merged = { ...cfg };
663
+ if (!merged.tenantCode && merged.tenantKey) {
664
+ merged.tenantCode = merged.tenantKey;
665
+ }
666
+ const clientId = trim(merged.clientId) || trim(merged.clientCode);
667
+ return {
668
+ apiBaseUrl: trim(merged.apiBaseUrl),
669
+ authServerUrl: trim(merged.authServerUrl),
670
+ callbackUrl: trim(merged.callbackUrl) || trim(merged.redirectUri),
671
+ clientCode: trim(merged.clientCode),
672
+ tenantCode: trim(merged.tenantCode),
673
+ appId: trim(merged.appId),
674
+ clientId,
675
+ tenantId: trim(merged.tenantId)
676
+ };
677
+ }
678
+ var AuthConfigError = class extends Error {
679
+ constructor(missing, message) {
680
+ super(
681
+ 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__.`
682
+ );
683
+ this.code = "AUTH_CONFIG_MISSING";
684
+ this.name = "AuthConfigError";
685
+ this.missing = missing;
686
+ }
687
+ };
688
+ function resolveConfig(overrides) {
689
+ return toResolvedConfig(resolveSDKConfig(overrides));
690
+ }
691
+ function getAuthConfig(options) {
692
+ const resolved = toResolvedConfig(resolveSDKConfig(options?.overrides));
693
+ if (options?.strict) {
694
+ const missing = [];
695
+ if (!resolved.authServerUrl) missing.push("authServerUrl");
696
+ if (!resolved.callbackUrl) missing.push("callbackUrl");
697
+ if (!resolved.tenantCode) missing.push("tenantCode|tenantKey");
698
+ if (missing.length) throw new AuthConfigError(missing);
699
+ }
700
+ return resolved;
701
+ }
702
+ function syncHostConfigCacheFromMemory() {
703
+ const c = resolveSDKConfig();
704
+ const patch = {};
705
+ const keys = [
706
+ "apiBaseUrl",
707
+ "authServerUrl",
708
+ "callbackUrl",
709
+ "redirectUri",
710
+ "tenantCode",
711
+ "tenantKey",
712
+ "clientCode",
713
+ "clientId",
714
+ "tenantId",
715
+ "appId",
716
+ "basePath"
717
+ ];
718
+ for (const k of keys) {
719
+ const v = c[k];
720
+ if (v != null && String(v).trim()) {
721
+ patch[k] = String(v).trim();
722
+ }
723
+ }
724
+ if (Object.keys(patch).length) writeHostConfigCache(patch);
725
+ }
726
+ function writeHostConfigCache(partial) {
727
+ try {
728
+ const key = getStorageKey("host_config");
729
+ const raw = getStorage(key);
730
+ let existing = {};
731
+ try {
732
+ if (raw) existing = JSON.parse(raw);
733
+ } catch {
734
+ existing = {};
735
+ }
736
+ const next = {
737
+ ...existing,
738
+ ...partial,
739
+ [CACHE_MARKER]: true,
740
+ [CACHE_AT]: (/* @__PURE__ */ new Date()).toISOString()
741
+ };
742
+ setStorage(key, JSON.stringify(next));
743
+ logger.debug("[config] host_config cache updated (non-authoritative)", {
744
+ keys: Object.keys(partial)
745
+ });
746
+ } catch (e) {
747
+ logger.warn("[config] Failed to write host_config cache", {
748
+ error: e instanceof Error ? e.message : String(e)
749
+ });
750
+ }
751
+ }
752
+ function initPlugin(config, opts) {
753
+ setConfig(config);
754
+ logger.info("[config] initPlugin \u2014 runtime config updated", {
755
+ syncStorageCache: opts?.syncStorageCache !== false,
756
+ tenantCode: config.tenantCode ?? config.tenantKey ?? "(none)"
757
+ });
758
+ if (opts?.syncStorageCache !== false) {
759
+ writeHostConfigCache(config);
760
+ }
761
+ }
762
+ function logConfigResolutionDebug(context, overrides) {
763
+ const sources = getConfigFieldSources(overrides);
764
+ logger.debug(`[config] ${context}`, {
765
+ fieldSources: sources,
766
+ effectiveTenantCode: getEffectiveTenantCode(overrides) || "(empty)",
767
+ hasAuthServerUrl: !!resolveSDKConfig(overrides).authServerUrl,
768
+ hasCallbackUrl: !!resolveSDKConfig(overrides).callbackUrl
769
+ });
770
+ }
771
+
772
+ // src/config/host-fields.ts
773
+ function trim2(v) {
774
+ if (v == null) return null;
775
+ const s = String(v).trim();
776
+ return s || null;
777
+ }
778
+ function oauthClientIdForTokenRequest(raw) {
779
+ if (!raw) return null;
780
+ let s = raw.trim();
781
+ if (!s) return null;
782
+ if (s.toLowerCase().startsWith("public-")) {
783
+ s = s.slice("public-".length).trim();
784
+ }
785
+ return s || null;
786
+ }
787
+ function getClientId() {
788
+ const c = resolveSDKConfig();
789
+ const fromId = oauthClientIdForTokenRequest(trim2(c.clientId));
790
+ if (fromId) return fromId;
791
+ return oauthClientIdForTokenRequest(trim2(c.clientCode));
792
+ }
793
+ function normalizeClientIdForOAuthTokenBody(raw) {
794
+ return oauthClientIdForTokenRequest(trim2(raw));
795
+ }
796
+ function getRedirectUri() {
797
+ const c = resolveSDKConfig();
798
+ return trim2(c.callbackUrl) ?? trim2(c.redirectUri);
799
+ }
800
+ function getTenantId() {
801
+ return trim2(resolveSDKConfig().tenantId);
802
+ }
803
+
659
804
  // src/domain/errors.ts
660
805
  function isLocalhostHostname(hostname) {
661
806
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.startsWith("172.16.");
@@ -711,10 +856,8 @@ function resolveAuthLifecycleState(input) {
711
856
  hasRefreshToken,
712
857
  isTokenExpired
713
858
  } = input;
714
- const forceLogin = getStorage3("force_login") === "true";
715
859
  const callbackProcessed = getStorage3("callback_processed") === "true";
716
860
  const tenantSwitched = getStorage3("tenant_switched") === "true";
717
- if (forceLogin) return "FORCE_LOGIN" /* FORCE_LOGIN */;
718
861
  if (isCallbackUrl2(url)) return "PROCESSING_CALLBACK" /* PROCESSING_CALLBACK */;
719
862
  if (hasValidToken) return "AUTHENTICATED" /* AUTHENTICATED */;
720
863
  if (callbackProcessed && !hasToken) return "LOGIN_REQUIRED" /* LOGIN_REQUIRED */;
@@ -775,30 +918,58 @@ function setFlowFlags() {
775
918
  setFlowInProgress();
776
919
  }
777
920
 
778
- // src/strategies/oauth2-pkce/pkce.ts
779
- function generateCodeVerifier(length = 128) {
780
- const array = new Uint8Array(length);
781
- crypto.getRandomValues(array);
782
- const base64 = btoa(String.fromCharCode(...array));
783
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
921
+ // src/application/events/AuthEvents.ts
922
+ var AuthEventNames = {
923
+ LOGIN_STARTED: "LOGIN_STARTED",
924
+ LOGIN_SUCCESS: "LOGIN_SUCCESS",
925
+ LOGIN_FAILED: "LOGIN_FAILED",
926
+ CALLBACK_SUCCESS: "CALLBACK_SUCCESS",
927
+ CALLBACK_FAILED: "CALLBACK_FAILED",
928
+ TOKEN_REFRESH_SUCCESS: "TOKEN_REFRESH_SUCCESS",
929
+ TOKEN_REFRESH_FAILED: "TOKEN_REFRESH_FAILED",
930
+ TENANT_SWITCHED: "TENANT_SWITCHED",
931
+ LOGOUT_COMPLETED: "LOGOUT_COMPLETED",
932
+ BOOTSTRAP_STATE_DETECTED: "BOOTSTRAP_STATE_DETECTED",
933
+ REDIRECT_LOOP_RISK: "REDIRECT_LOOP_RISK"
934
+ };
935
+ var listeners = /* @__PURE__ */ new Map();
936
+ function getHandlers(event) {
937
+ let set = listeners.get(event);
938
+ if (!set) {
939
+ set = /* @__PURE__ */ new Set();
940
+ listeners.set(event, set);
941
+ }
942
+ return set;
784
943
  }
785
- async function generateCodeChallenge(verifier) {
786
- try {
787
- const encoder = new TextEncoder();
788
- const data = encoder.encode(verifier);
789
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
790
- const hashArray = Array.from(new Uint8Array(hashBuffer));
791
- const base64 = btoa(String.fromCharCode(...hashArray));
792
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
793
- } catch {
794
- throw new Error("Failed to generate code challenge");
944
+ function getWildcardHandlers() {
945
+ let set = listeners.get("*");
946
+ if (!set) {
947
+ set = /* @__PURE__ */ new Set();
948
+ listeners.set("*", set);
795
949
  }
950
+ return set;
796
951
  }
797
- function generateState() {
798
- const array = new Uint8Array(32);
799
- crypto.getRandomValues(array);
800
- const base64 = btoa(String.fromCharCode(...array));
801
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
952
+ function emitAuthEvent(event, payload) {
953
+ const handlers = getHandlers(event);
954
+ const wildcard = getWildcardHandlers();
955
+ const p = payload ?? {};
956
+ handlers.forEach((fn) => {
957
+ try {
958
+ fn(p);
959
+ } catch {
960
+ }
961
+ });
962
+ wildcard.forEach((fn) => {
963
+ try {
964
+ fn({ ...p, event });
965
+ } catch {
966
+ }
967
+ });
968
+ }
969
+ function subscribeAuthEvent(event, handler) {
970
+ const set = event === "*" ? getWildcardHandlers() : getHandlers(event);
971
+ set.add(handler);
972
+ return () => set.delete(handler);
802
973
  }
803
974
 
804
975
  // src/config/constants.ts
@@ -828,342 +999,169 @@ var DEFAULT_TOKEN_GRANT_TYPE = OAUTH_CONSTANTS.TOKEN_GRANT_TYPE;
828
999
  var DEFAULT_REFRESH_GRANT_TYPE = OAUTH_CONSTANTS.REFRESH_GRANT_TYPE;
829
1000
 
830
1001
  // src/config/index.ts
831
- var inMemoryConfig = {};
832
- function readHostConfigFromStorage() {
833
- try {
834
- const raw = getStorage(getStorageKey("host_config"));
835
- if (!raw) return {};
836
- const parsed = JSON.parse(raw);
837
- return parsed && typeof parsed === "object" ? parsed : {};
838
- } catch {
839
- return {};
840
- }
1002
+ function getAuthConfig2(options) {
1003
+ return getAuthConfig(options);
841
1004
  }
842
- function resolveMerged(overrides) {
843
- const stored = readHostConfigFromStorage();
844
- const cfg = { ...stored, ...inMemoryConfig, ...overrides ?? {} };
845
- const trimmed = (v) => v == null ? "" : String(v).trim();
846
- return {
847
- apiBaseUrl: trimmed(cfg.apiBaseUrl),
848
- authServerUrl: trimmed(cfg.authServerUrl),
849
- callbackUrl: trimmed(cfg.callbackUrl),
850
- clientCode: trimmed(cfg.clientCode)
851
- };
852
- }
853
- function resolveConfig(overrides) {
854
- return resolveMerged(overrides);
1005
+ function requireAuthConfig(overrides) {
1006
+ return getAuthConfig({ strict: true, overrides });
855
1007
  }
856
1008
  function getConfig() {
857
- return resolveMerged();
858
- }
859
- function setConfig(overrides) {
860
- inMemoryConfig = { ...inMemoryConfig, ...overrides };
861
- }
862
- function clearConfigOverrides() {
863
- inMemoryConfig = {};
1009
+ return resolveConfig();
864
1010
  }
865
1011
  function getRedirectUri2() {
866
- const cfg = resolveMerged();
1012
+ const cfg = resolveConfig();
867
1013
  if (cfg.callbackUrl) return cfg.callbackUrl;
868
1014
  if (typeof window !== "undefined") return `${window.location.origin}/callback`;
869
1015
  return "";
870
1016
  }
871
1017
  var Config = {
872
1018
  getApiBaseUrl() {
873
- return resolveMerged().apiBaseUrl;
1019
+ return resolveConfig().apiBaseUrl;
874
1020
  },
875
1021
  getAuthServerUrl() {
876
- return resolveMerged().authServerUrl;
1022
+ return resolveConfig().authServerUrl;
877
1023
  },
878
1024
  getClientCode() {
879
- return resolveMerged().clientCode;
1025
+ return resolveConfig().clientCode;
880
1026
  },
881
1027
  getRedirectUri() {
882
1028
  return getRedirectUri2();
883
1029
  },
884
1030
  getAll() {
885
- return resolveMerged();
1031
+ return resolveConfig();
886
1032
  }
887
1033
  };
888
-
889
- // src/utils/validation.ts
890
- function isValidGuid(guid) {
891
- if (!guid || typeof guid !== "string") {
892
- return false;
893
- }
894
- const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
895
- return guidRegex.test(guid.trim());
896
- }
897
- function validateTenantId(tenantId) {
898
- if (!tenantId || tenantId.trim() === "") {
899
- return {
900
- isValid: false,
901
- error: "Tenant ID is required"
902
- };
903
- }
904
- if (!isValidGuid(tenantId)) {
905
- return {
906
- isValid: false,
907
- error: "Tenant ID must be a valid GUID format (00000000-0000-0000-0000-000000000001)"
908
- };
1034
+ function applyRuntimeTenantSwitch(tenantKey, opts) {
1035
+ const t = tenantKey.trim();
1036
+ if (!t) return;
1037
+ setConfig({ tenantCode: t, tenantKey: t });
1038
+ logConfigResolutionDebug("applyRuntimeTenantSwitch", { tenantCode: t, tenantKey: t });
1039
+ if (opts?.syncStorageCache !== false) {
1040
+ writeHostConfigCache({ tenantCode: t, tenantKey: t });
909
1041
  }
910
- return { isValid: true };
911
1042
  }
912
1043
 
913
- // src/application/domain-facade.ts
914
- function isLocalhost2() {
915
- const runtime = getAuthRuntime();
916
- const hostname = runtime ? runtime.urlProvider.getCurrentUrl().hostname : typeof window !== "undefined" ? window.location.hostname : "";
917
- return isLocalhost(hostname);
918
- }
919
- function extractTenantKey() {
920
- const runtime = getAuthRuntime();
921
- if (runtime) {
922
- const url = runtime.urlProvider.getCurrentUrl();
923
- return extractTenantKeyFromUrl(url, runtime.configProvider, getStorage, getStorageKey);
1044
+ // src/infrastructure/tenant/TenantConfigCache.ts
1045
+ var DEFAULT_TTL_MS = 5 * 60 * 1e3;
1046
+ var TenantConfigCache = class {
1047
+ constructor(ttlMs = DEFAULT_TTL_MS) {
1048
+ this.cache = /* @__PURE__ */ new Map();
1049
+ this.inFlight = /* @__PURE__ */ new Map();
1050
+ this.ttlMs = ttlMs;
924
1051
  }
925
- return extractTenantKeyLegacy();
926
- }
927
- function extractTenantKeyLegacy() {
928
- if (typeof window === "undefined") return Config.getClientCode();
929
- try {
930
- const propsConfigStr = getStorage(getStorageKey("props_config"));
931
- if (propsConfigStr) {
932
- const propsConfig = JSON.parse(propsConfigStr);
933
- if (propsConfig.tenantKey?.trim()) return propsConfig.tenantKey.trim();
1052
+ get(tenantKey) {
1053
+ const entry = this.cache.get(tenantKey);
1054
+ if (!entry) return null;
1055
+ if (Date.now() >= entry.expiresAt) {
1056
+ this.cache.delete(tenantKey);
1057
+ return null;
934
1058
  }
935
- const stored = getStorage(getStorageKey("tenant_key"));
936
- if (stored?.trim()) return stored.trim();
937
- const urlParams = new URLSearchParams(window.location.search);
938
- const t = urlParams.get("t");
939
- if (t?.trim()) return t.trim();
940
- const hostname = window.location.hostname;
941
- const parts = hostname.split(".");
942
- if (parts.length >= 2 && parts[0] && parts[0].toLowerCase() !== "productlms") return parts[0].trim();
943
- return Config.getClientCode();
944
- } catch {
945
- return Config.getClientCode();
1059
+ return entry.config;
946
1060
  }
947
- }
948
- function isCallbackUrl() {
949
- const runtime = getAuthRuntime();
950
- if (runtime) return isCallbackUrlFromUrl(runtime.urlProvider.getCurrentUrl());
951
- if (typeof window === "undefined") return false;
952
- const params = new URLSearchParams(window.location.search);
953
- return params.has("code") && params.has("state");
954
- }
955
-
956
- // src/application/events/AuthEvents.ts
957
- var AuthEventNames = {
958
- LOGIN_STARTED: "LOGIN_STARTED",
959
- LOGIN_SUCCESS: "LOGIN_SUCCESS",
960
- LOGIN_FAILED: "LOGIN_FAILED",
961
- CALLBACK_SUCCESS: "CALLBACK_SUCCESS",
962
- CALLBACK_FAILED: "CALLBACK_FAILED",
963
- TOKEN_REFRESH_SUCCESS: "TOKEN_REFRESH_SUCCESS",
964
- TOKEN_REFRESH_FAILED: "TOKEN_REFRESH_FAILED",
965
- TENANT_SWITCHED: "TENANT_SWITCHED",
966
- LOGOUT_COMPLETED: "LOGOUT_COMPLETED",
967
- BOOTSTRAP_STATE_DETECTED: "BOOTSTRAP_STATE_DETECTED",
968
- REDIRECT_LOOP_RISK: "REDIRECT_LOOP_RISK"
969
- };
970
- var listeners = /* @__PURE__ */ new Map();
971
- function getHandlers(event) {
972
- let set = listeners.get(event);
973
- if (!set) {
974
- set = /* @__PURE__ */ new Set();
975
- listeners.set(event, set);
1061
+ set(tenantKey, config) {
1062
+ this.cache.set(tenantKey, {
1063
+ config,
1064
+ expiresAt: Date.now() + this.ttlMs
1065
+ });
976
1066
  }
977
- return set;
978
- }
979
- function getWildcardHandlers() {
980
- let set = listeners.get("*");
981
- if (!set) {
982
- set = /* @__PURE__ */ new Set();
983
- listeners.set("*", set);
1067
+ invalidate(tenantKey) {
1068
+ if (tenantKey) {
1069
+ this.cache.delete(tenantKey);
1070
+ this.inFlight.delete(tenantKey);
1071
+ } else {
1072
+ this.cache.clear();
1073
+ this.inFlight.clear();
1074
+ }
984
1075
  }
985
- return set;
986
- }
987
- function emitAuthEvent(event, payload) {
988
- const handlers = getHandlers(event);
989
- const wildcard = getWildcardHandlers();
990
- const p = payload ?? {};
991
- handlers.forEach((fn) => {
992
- try {
993
- fn(p);
994
- } catch {
1076
+ async getOrResolve(tenantKey, resolver) {
1077
+ if (!tenantKey || !tenantKey.trim()) return null;
1078
+ const key = tenantKey.trim();
1079
+ const cached = this.get(key);
1080
+ if (cached) {
1081
+ logger.debug("Tenant config from cache", { tenantKey: key });
1082
+ return cached;
995
1083
  }
996
- });
997
- wildcard.forEach((fn) => {
998
- try {
999
- fn({ ...p, event });
1000
- } catch {
1084
+ const existing = this.inFlight.get(key);
1085
+ if (existing) {
1086
+ logger.debug("Tenant resolution reused (in-flight)", { tenantKey: key });
1087
+ return existing;
1001
1088
  }
1002
- });
1003
- }
1004
- function subscribeAuthEvent(event, handler) {
1005
- const set = event === "*" ? getWildcardHandlers() : getHandlers(event);
1006
- set.add(handler);
1007
- return () => set.delete(handler);
1008
- }
1009
-
1010
- // src/application/security/redirectLoopGuard.ts
1011
- var DEFAULT_MAX_REDIRECTS = 5;
1012
- var WINDOW_MS = 60 * 1e3;
1013
- var redirectCount = 0;
1014
- var firstRedirectAt = 0;
1015
- function resetRedirectLoopGuard() {
1016
- redirectCount = 0;
1017
- firstRedirectAt = 0;
1018
- }
1019
- function checkAndIncrementRedirect(maxAttempts = DEFAULT_MAX_REDIRECTS) {
1020
- const now = Date.now();
1021
- if (now - firstRedirectAt > WINDOW_MS) {
1022
- redirectCount = 0;
1023
- firstRedirectAt = now;
1024
- }
1025
- if (redirectCount >= maxAttempts) {
1026
- emitAuthEvent(AuthEventNames.REDIRECT_LOOP_RISK, {
1027
- message: `Redirect attempt limit (${maxAttempts}) exceeded`
1028
- });
1029
- return false;
1089
+ const promise = (async () => {
1090
+ try {
1091
+ const config = await resolver(key);
1092
+ if (config) this.set(key, config);
1093
+ return config;
1094
+ } finally {
1095
+ this.inFlight.delete(key);
1096
+ }
1097
+ })();
1098
+ this.inFlight.set(key, promise);
1099
+ return promise;
1030
1100
  }
1031
- redirectCount++;
1032
- return true;
1033
- }
1101
+ };
1034
1102
 
1035
- // src/strategies/oauth2-pkce/oauth.ts
1036
- function storeTenantConfig(config) {
1037
- const validation = validateTenantId(config.tenantId);
1038
- if (!validation.isValid) {
1039
- logger.warn("Tenant ID validation failed", { tenantId: config.tenantId });
1040
- }
1041
- const authServerToStore = Config.getAuthServerUrl() || config.authServer;
1042
- setStorage(STORAGE_KEYS.authServerUrl, authServerToStore);
1043
- setStorage(STORAGE_KEYS.clientId, config.client.clientId);
1044
- setStorage(STORAGE_KEYS.redirectUri, Config.getRedirectUri());
1045
- setStorage(STORAGE_KEYS.tenantId, config.tenantId);
1046
- if (config.tenantKey) {
1047
- setStorage(STORAGE_KEYS.tenantKey, config.tenantKey);
1048
- }
1049
- }
1050
- async function initiateAuthFlow(config) {
1103
+ // src/infrastructure/tenant/tenantResolution.ts
1104
+ var defaultTenantConfigCache = new TenantConfigCache();
1105
+ function consumePropsConfig() {
1106
+ const propsConfigKey = getStorageKey("props_config");
1051
1107
  try {
1052
- logger.info("Initiating OAuth authorization flow");
1053
- const authServerFromProps = Config.getAuthServerUrl();
1054
- const authServerEffective = (authServerFromProps || config?.authServer || "").replace(/\/+$/, "");
1055
- const redirectUriEffective = Config.getRedirectUri();
1056
- const storedTenantId = getStorage(STORAGE_KEYS.tenantId);
1057
- const storedClientId = getStorage(STORAGE_KEYS.clientId);
1058
- const tenantIdEffective = storedTenantId || config?.tenantId || "";
1059
- const clientIdEffective = storedClientId || config?.client?.clientId || "";
1060
- logger.debug("[initiateAuthFlow] Resolved inputs", {
1061
- authServerFromProps: authServerFromProps ? "(provided)" : "(empty)",
1062
- authServerEffective,
1063
- redirectUriEffective,
1064
- tenantIdEffective,
1065
- clientIdEffective,
1066
- scopes: config?.client?.scopes,
1067
- hasRuntime: !!getAuthRuntime()
1068
- });
1069
- const token = getAccessToken();
1070
- if (token && !isAccessTokenExpired()) {
1071
- logger.debug("Valid token already exists, skipping auth flow");
1072
- clearFlowFlags();
1073
- return;
1074
- }
1075
- if (isCallbackUrl()) {
1076
- logger.debug("Callback URL detected - clearing flow flag to allow callback processing");
1077
- clearFlowFlags();
1078
- return;
1079
- }
1080
- if (isFlowInProgress()) {
1081
- if (isFlowFlagStale(3e3)) {
1082
- logger.info("Stale auth flow flag detected, clearing and proceeding");
1083
- clearFlowFlags();
1084
- } else {
1085
- logger.debug("Auth flow already in progress (recent), skipping duplicate redirect");
1086
- return;
1087
- }
1088
- }
1089
- setFlowFlags();
1090
- if (!(Config.getAuthServerUrl() || config?.authServer) || !config.client?.clientId || !config.client?.redirectUri) {
1091
- logger.error("[initiateAuthFlow] Missing required tenant config", {
1092
- authServerEffective,
1093
- clientId: config?.client?.clientId,
1094
- redirectUriFromTenantApi: config?.client?.redirectUri,
1095
- redirectUriEffective
1096
- });
1097
- throw new Error("Invalid tenant configuration for auth flow");
1098
- }
1099
- storeTenantConfig(config);
1100
- logger.debug("[initiateAuthFlow] Stored tenant config, generating PKCE");
1101
- const codeVerifier = generateCodeVerifier();
1102
- const codeChallenge = await generateCodeChallenge(codeVerifier);
1103
- const state = generateState();
1104
- removeStorage(STORAGE_KEYS.codeVerifier);
1105
- removeStorage(STORAGE_KEYS.state);
1106
- setStorage(STORAGE_KEYS.codeVerifier, codeVerifier);
1107
- setStorage(STORAGE_KEYS.state, state);
1108
- const authUrl = buildAuthorizationUrl(config, codeChallenge, state);
1109
- logger.debug("[initiateAuthFlow] Built authorization URL", {
1110
- authServerEffective,
1111
- urlPrefix: authUrl?.slice(0, 80)
1112
- });
1113
- if (!authUrl || !(authUrl.startsWith("http://") || authUrl.startsWith("https://"))) {
1114
- throw new Error("Invalid authorization URL generated");
1115
- }
1116
- if (!checkAndIncrementRedirect()) {
1117
- logger.warn("Redirect loop protection: max redirect attempts exceeded");
1118
- clearFlowFlags();
1119
- return;
1120
- }
1121
- logger.info("Redirecting to authorization server");
1122
- emitAuthEvent(AuthEventNames.LOGIN_STARTED, { tenantKey: config.tenantKey });
1123
- const runtime = getAuthRuntime();
1124
- if (runtime) {
1125
- logger.debug("[initiateAuthFlow] Redirect via runtime.urlProvider.redirect");
1126
- runtime.urlProvider.redirect(authUrl);
1127
- } else if (typeof window !== "undefined") {
1128
- logger.debug("[initiateAuthFlow] Redirect via window.location.replace");
1129
- window.location.replace(authUrl);
1130
- } else {
1131
- logger.error("[initiateAuthFlow] No runtime and no window; cannot redirect", { authUrl });
1132
- }
1133
- } catch (error) {
1134
- clearFlowFlags();
1135
- logger.error("Failed to initiate auth flow", {
1136
- error: error instanceof Error ? error.message : String(error)
1137
- });
1138
- throw error;
1108
+ const propsConfigStr = getStorage(propsConfigKey);
1109
+ if (!propsConfigStr) return null;
1110
+ const parsed = JSON.parse(propsConfigStr);
1111
+ return parsed && typeof parsed === "object" ? parsed : null;
1112
+ } catch {
1113
+ return null;
1114
+ } finally {
1115
+ removeStorage(propsConfigKey);
1139
1116
  }
1140
- }
1141
- function buildAuthorizationUrl(config, codeChallenge, state) {
1142
- const authServerBase = (Config.getAuthServerUrl() || config.authServer || "").replace(/\/+$/, "");
1143
- if (isLocalhost2()) {
1144
- logger.debug("Building authorization URL", {
1145
- authServer: authServerBase,
1146
- clientId: config.client.clientId,
1147
- redirectUri: config.client.redirectUri,
1148
- scopes: config.client.scopes
1149
- });
1117
+ }
1118
+ function validateTenantKey(value) {
1119
+ if (typeof value !== "string") return null;
1120
+ const trimmed = value.trim();
1121
+ return trimmed ? trimmed : null;
1122
+ }
1123
+ function resolveTenantKey() {
1124
+ const propsConfig = consumePropsConfig();
1125
+ const tenantFromProps = validateTenantKey(propsConfig?.tenantKey);
1126
+ if (tenantFromProps) {
1127
+ logger.debug("[tenant] Resolved tenant from props_config", { tenantKey: tenantFromProps });
1128
+ return tenantFromProps;
1150
1129
  }
1151
- const storedTenantId = getStorage(STORAGE_KEYS.tenantId);
1152
- const storedClientId = getStorage(STORAGE_KEYS.clientId);
1153
- const tenantIdToUse = storedTenantId || config.tenantId;
1154
- const clientIdToUse = storedClientId || config.client.clientId;
1155
- const redirectUriToUse = Config.getRedirectUri();
1156
- const params = new URLSearchParams({
1157
- client_id: `public-${clientIdToUse}`,
1158
- tenant_id: tenantIdToUse,
1159
- redirect_uri: redirectUriToUse,
1160
- response_type: DEFAULT_RESPONSE_TYPE,
1161
- scope: config.client.scopes,
1162
- state,
1163
- code_challenge: codeChallenge,
1164
- code_challenge_method: DEFAULT_CODE_CHALLENGE_METHOD
1165
- });
1166
- return `${authServerBase}/connect/authorize?${params.toString()}`;
1130
+ const fromHost = validateTenantKey(getEffectiveTenantCode());
1131
+ if (fromHost) {
1132
+ logger.debug("[tenant] Resolved tenant from host/runtime config", { tenantKey: fromHost });
1133
+ return fromHost;
1134
+ }
1135
+ logger.warn("[tenant] Unable to resolve tenant key (props + host/runtime empty)");
1136
+ return null;
1137
+ }
1138
+ async function resolveTenantConfig(tenantKey, tenantResolver) {
1139
+ const resolved = await tenantResolver.resolve(tenantKey);
1140
+ if (!resolved) return null;
1141
+ return { ...resolved, tenantKey };
1142
+ }
1143
+ async function resolveTenantConfigCached(tenantKey, tenantResolver) {
1144
+ return defaultTenantConfigCache.getOrResolve(
1145
+ tenantKey,
1146
+ (key) => resolveTenantConfig(key, tenantResolver)
1147
+ );
1148
+ }
1149
+
1150
+ // src/application/domain-facade.ts
1151
+ function isLocalhost2() {
1152
+ const runtime = getAuthRuntime();
1153
+ const hostname = runtime ? runtime.urlProvider.getCurrentUrl().hostname : typeof window !== "undefined" ? window.location.hostname : "";
1154
+ return isLocalhost(hostname);
1155
+ }
1156
+ function extractTenantKey() {
1157
+ return resolveTenantKey();
1158
+ }
1159
+ function isCallbackUrl() {
1160
+ const runtime = getAuthRuntime();
1161
+ if (runtime) return isCallbackUrlFromUrl(runtime.urlProvider.getCurrentUrl());
1162
+ if (typeof window === "undefined") return false;
1163
+ const params = new URLSearchParams(window.location.search);
1164
+ return params.has("code") && params.has("state");
1167
1165
  }
1168
1166
 
1169
1167
  // src/strategies/oauth2-pkce/token-exchange.ts
@@ -1325,6 +1323,221 @@ async function refreshAccessToken() {
1325
1323
  })();
1326
1324
  return refreshPromise;
1327
1325
  }
1326
+
1327
+ // src/strategies/oauth2-pkce/pkce.ts
1328
+ function generateCodeVerifier(length = 128) {
1329
+ const array = new Uint8Array(length);
1330
+ crypto.getRandomValues(array);
1331
+ const base64 = btoa(String.fromCharCode(...array));
1332
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1333
+ }
1334
+ async function generateCodeChallenge(verifier) {
1335
+ try {
1336
+ const encoder = new TextEncoder();
1337
+ const data = encoder.encode(verifier);
1338
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1339
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
1340
+ const base64 = btoa(String.fromCharCode(...hashArray));
1341
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1342
+ } catch {
1343
+ throw new Error("Failed to generate code challenge");
1344
+ }
1345
+ }
1346
+ function generateState() {
1347
+ const array = new Uint8Array(32);
1348
+ crypto.getRandomValues(array);
1349
+ const base64 = btoa(String.fromCharCode(...array));
1350
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1351
+ }
1352
+
1353
+ // src/utils/validation.ts
1354
+ function isValidGuid(guid) {
1355
+ if (!guid || typeof guid !== "string") {
1356
+ return false;
1357
+ }
1358
+ const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1359
+ return guidRegex.test(guid.trim());
1360
+ }
1361
+ function validateTenantId(tenantId) {
1362
+ if (!tenantId || tenantId.trim() === "") {
1363
+ return {
1364
+ isValid: false,
1365
+ error: "Tenant ID is required"
1366
+ };
1367
+ }
1368
+ if (!isValidGuid(tenantId)) {
1369
+ return {
1370
+ isValid: false,
1371
+ error: "Tenant ID must be a valid GUID format (00000000-0000-0000-0000-000000000001)"
1372
+ };
1373
+ }
1374
+ return { isValid: true };
1375
+ }
1376
+
1377
+ // src/application/security/redirectLoopGuard.ts
1378
+ var DEFAULT_MAX_REDIRECTS = 5;
1379
+ var WINDOW_MS = 60 * 1e3;
1380
+ var redirectCount = 0;
1381
+ var firstRedirectAt = 0;
1382
+ function resetRedirectLoopGuard() {
1383
+ redirectCount = 0;
1384
+ firstRedirectAt = 0;
1385
+ }
1386
+ function checkAndIncrementRedirect(maxAttempts = DEFAULT_MAX_REDIRECTS) {
1387
+ const now = Date.now();
1388
+ if (now - firstRedirectAt > WINDOW_MS) {
1389
+ redirectCount = 0;
1390
+ firstRedirectAt = now;
1391
+ }
1392
+ if (redirectCount >= maxAttempts) {
1393
+ emitAuthEvent(AuthEventNames.REDIRECT_LOOP_RISK, {
1394
+ message: `Redirect attempt limit (${maxAttempts}) exceeded`
1395
+ });
1396
+ return false;
1397
+ }
1398
+ redirectCount++;
1399
+ return true;
1400
+ }
1401
+
1402
+ // src/strategies/oauth2-pkce/oauth.ts
1403
+ function storeTenantConfig(config) {
1404
+ const validation = validateTenantId(config.tenantId);
1405
+ if (!validation.isValid) {
1406
+ logger.warn("Tenant ID validation failed", { tenantId: config.tenantId });
1407
+ }
1408
+ const authServerToStore = Config.getAuthServerUrl() || config.authServer;
1409
+ const redirectUriToStore = Config.getRedirectUri();
1410
+ writeHostConfigCache({
1411
+ authServerUrl: authServerToStore,
1412
+ clientId: config.client.clientId,
1413
+ clientCode: config.client.clientCode ?? config.client.clientId,
1414
+ redirectUri: redirectUriToStore,
1415
+ callbackUrl: redirectUriToStore,
1416
+ tenantId: config.tenantId,
1417
+ tenantKey: config.tenantKey ?? "",
1418
+ tenantCode: config.tenantKey ?? ""
1419
+ });
1420
+ if (config.tenantKey) {
1421
+ setStorage(STORAGE_KEYS.tenantKey, config.tenantKey);
1422
+ }
1423
+ }
1424
+ async function initiateAuthFlow(config) {
1425
+ try {
1426
+ logger.info("Initiating OAuth authorization flow");
1427
+ const authServerFromProps = Config.getAuthServerUrl();
1428
+ const authServerEffective = (authServerFromProps || config?.authServer || "").replace(/\/+$/, "");
1429
+ const redirectUriEffective = Config.getRedirectUri();
1430
+ const tenantIdEffective = config?.tenantId || "";
1431
+ const clientIdEffective = config?.client?.clientId || "";
1432
+ logger.debug("[initiateAuthFlow] Resolved inputs", {
1433
+ authServerFromProps: authServerFromProps ? "(provided)" : "(empty)",
1434
+ authServerEffective,
1435
+ redirectUriEffective,
1436
+ tenantIdEffective,
1437
+ clientIdEffective,
1438
+ scopes: config?.client?.scopes,
1439
+ hasRuntime: !!getAuthRuntime()
1440
+ });
1441
+ const token = getAccessToken();
1442
+ if (token && !isAccessTokenExpired()) {
1443
+ logger.debug("Valid token already exists, skipping auth flow");
1444
+ clearFlowFlags();
1445
+ return;
1446
+ }
1447
+ if (isCallbackUrl()) {
1448
+ logger.debug("Callback URL detected - clearing flow flag to allow callback processing");
1449
+ clearFlowFlags();
1450
+ return;
1451
+ }
1452
+ if (isFlowInProgress()) {
1453
+ if (isFlowFlagStale(3e3)) {
1454
+ logger.info("Stale auth flow flag detected, clearing and proceeding");
1455
+ clearFlowFlags();
1456
+ } else {
1457
+ logger.debug("Auth flow already in progress (recent), skipping duplicate redirect");
1458
+ return;
1459
+ }
1460
+ }
1461
+ setFlowFlags();
1462
+ if (!(Config.getAuthServerUrl() || config?.authServer) || !config.client?.clientId || !config.client?.redirectUri) {
1463
+ logger.error("[initiateAuthFlow] Missing required tenant config", {
1464
+ authServerEffective,
1465
+ clientId: config?.client?.clientId,
1466
+ redirectUriFromTenantApi: config?.client?.redirectUri,
1467
+ redirectUriEffective
1468
+ });
1469
+ throw new Error("Invalid tenant configuration for auth flow");
1470
+ }
1471
+ storeTenantConfig(config);
1472
+ logger.debug("[initiateAuthFlow] Stored tenant config, generating PKCE");
1473
+ removeStorage(getStorageKey("callback_processed"));
1474
+ removeStorage(getStorageKey("state_mismatch"));
1475
+ clearCallbackProcessing();
1476
+ const codeVerifier = generateCodeVerifier();
1477
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
1478
+ const state = generateState();
1479
+ removeStorage(STORAGE_KEYS.codeVerifier);
1480
+ removeStorage(STORAGE_KEYS.state);
1481
+ setStorage(STORAGE_KEYS.codeVerifier, codeVerifier);
1482
+ setStorage(STORAGE_KEYS.state, state);
1483
+ const authUrl = buildAuthorizationUrl(config, codeChallenge, state);
1484
+ logger.debug("[initiateAuthFlow] Built authorization URL", {
1485
+ authServerEffective,
1486
+ urlPrefix: authUrl?.slice(0, 80)
1487
+ });
1488
+ if (!authUrl || !(authUrl.startsWith("http://") || authUrl.startsWith("https://"))) {
1489
+ throw new Error("Invalid authorization URL generated");
1490
+ }
1491
+ if (!checkAndIncrementRedirect()) {
1492
+ logger.warn("Redirect loop protection: max redirect attempts exceeded");
1493
+ clearFlowFlags();
1494
+ return;
1495
+ }
1496
+ logger.info("Redirecting to authorization server");
1497
+ emitAuthEvent(AuthEventNames.LOGIN_STARTED, { tenantKey: config.tenantKey });
1498
+ const runtime = getAuthRuntime();
1499
+ if (runtime) {
1500
+ logger.debug("[initiateAuthFlow] Redirect via runtime.urlProvider.redirect");
1501
+ runtime.urlProvider.redirect(authUrl);
1502
+ } else if (typeof window !== "undefined") {
1503
+ logger.debug("[initiateAuthFlow] Redirect via window.location.replace");
1504
+ window.location.replace(authUrl);
1505
+ } else {
1506
+ logger.error("[initiateAuthFlow] No runtime and no window; cannot redirect", { authUrl });
1507
+ }
1508
+ } catch (error) {
1509
+ clearFlowFlags();
1510
+ logger.error("Failed to initiate auth flow", {
1511
+ error: error instanceof Error ? error.message : String(error)
1512
+ });
1513
+ throw error;
1514
+ }
1515
+ }
1516
+ function buildAuthorizationUrl(config, codeChallenge, state) {
1517
+ const authServerBase = (Config.getAuthServerUrl() || config.authServer || "").replace(/\/+$/, "");
1518
+ if (isLocalhost2()) {
1519
+ logger.debug("Building authorization URL", {
1520
+ authServer: authServerBase,
1521
+ clientId: config.client.clientId,
1522
+ redirectUri: config.client.redirectUri,
1523
+ scopes: config.client.scopes
1524
+ });
1525
+ }
1526
+ const tenantIdToUse = config.tenantId;
1527
+ const clientIdToUse = config.client.clientId;
1528
+ const redirectUriToUse = Config.getRedirectUri();
1529
+ const params = new URLSearchParams({
1530
+ client_id: `public-${clientIdToUse}`,
1531
+ tenant_id: tenantIdToUse,
1532
+ redirect_uri: redirectUriToUse,
1533
+ response_type: DEFAULT_RESPONSE_TYPE,
1534
+ scope: config.client.scopes,
1535
+ state,
1536
+ code_challenge: codeChallenge,
1537
+ code_challenge_method: DEFAULT_CODE_CHALLENGE_METHOD
1538
+ });
1539
+ return `${authServerBase}/connect/authorize?${params.toString()}`;
1540
+ }
1328
1541
  function isProcessed() {
1329
1542
  return getStorage(getStorageKey("callback_processed")) === "true";
1330
1543
  }
@@ -1343,15 +1556,6 @@ function clearProcessing() {
1343
1556
  removeStorage(getStorageKey("callback_processed"));
1344
1557
  }
1345
1558
  var urlCleaned = false;
1346
- function cleanUrl() {
1347
- if (urlCleaned) return;
1348
- if (isProcessing()) return;
1349
- const runtime = getAuthRuntime();
1350
- const clean = runtime ? `${runtime.urlProvider.getCurrentUrl().origin}${runtime.urlProvider.getCurrentUrl().pathname}` : typeof window !== "undefined" ? `${window.location.origin}${window.location.pathname}` : "/";
1351
- if (runtime) runtime.urlProvider.redirect(clean);
1352
- else if (typeof window !== "undefined") window.location.replace(clean);
1353
- urlCleaned = true;
1354
- }
1355
1559
  function cleanUrlSoft() {
1356
1560
  if (urlCleaned) return;
1357
1561
  if (isProcessing()) return;
@@ -1362,13 +1566,52 @@ function cleanUrlSoft() {
1362
1566
  urlCleaned = true;
1363
1567
  }
1364
1568
  function clearAllAuthFlags() {
1365
- removeStorage(getStorageKey("force_login"));
1366
- removeStorage(getStorageKey("logout_in_progress"));
1367
1569
  clearFlowFlags();
1368
1570
  }
1571
+ async function tryRecoverOAuthConfigFromTenant() {
1572
+ const runtime = getAuthRuntime();
1573
+ const tenantKey = (resolveTenantKey() ?? getTenantKey() ?? "").trim();
1574
+ if (!runtime?.tenantResolver || !tenantKey) {
1575
+ logger.debug("[Callback] OAuth config recovery skipped \u2014 no tenantResolver or tenantKey", {
1576
+ hasResolver: !!runtime?.tenantResolver,
1577
+ tenantKey: tenantKey || "(empty)"
1578
+ });
1579
+ return null;
1580
+ }
1581
+ try {
1582
+ const tc = await runtime.tenantResolver.resolve(tenantKey);
1583
+ if (!tc?.client?.clientId || !tc.tenantId) {
1584
+ logger.warn("[Callback] OAuth config recovery: tenant resolve missing client or tenantId", {
1585
+ tenantKey
1586
+ });
1587
+ return null;
1588
+ }
1589
+ storeTenantConfig(tc);
1590
+ return {
1591
+ clientId: tc.client.clientId.trim() || null,
1592
+ redirectUri: tc.client.redirectUri?.trim() || getRedirectUri(),
1593
+ tenantId: tc.tenantId.trim() || null
1594
+ };
1595
+ } catch (e) {
1596
+ logger.warn("[Callback] OAuth config recovery: tenant re-resolve failed", {
1597
+ tenantKey,
1598
+ error: e instanceof Error ? e.message : String(e)
1599
+ });
1600
+ return null;
1601
+ }
1602
+ }
1369
1603
  async function handleCallback() {
1604
+ const runtimeSearch = getAuthRuntime()?.urlProvider.getCurrentUrl().search;
1605
+ const windowSearch = typeof window !== "undefined" ? window.location.search : "";
1606
+ logger.debug("[Callback] handleCallback invoked", {
1607
+ isProcessed: isProcessed(),
1608
+ isProcessing: isProcessing(),
1609
+ isCallbackUrl: isCallbackUrl(),
1610
+ runtimeSearch: runtimeSearch ?? "(none)",
1611
+ windowSearch: windowSearch || "(none)"
1612
+ });
1370
1613
  if (isProcessed()) {
1371
- logger.debug("Callback already processed in this session - preventing duplicate processing");
1614
+ logger.warn("[Callback] Early exit: callback already processed");
1372
1615
  if (isCallbackUrl()) {
1373
1616
  if (!isProcessing()) cleanUrlSoft();
1374
1617
  }
@@ -1376,31 +1619,16 @@ async function handleCallback() {
1376
1619
  return true;
1377
1620
  }
1378
1621
  if (isProcessing()) {
1379
- logger.debug("Callback processing already in progress - waiting for completion");
1622
+ logger.warn("[Callback] Early exit: callback processing already in progress");
1380
1623
  return true;
1381
1624
  }
1382
- if (getStorage(getStorageKey("force_login")) === "true") {
1383
- logger.info("Force-login flag present \u2192 callback skipped (logout occurred)");
1384
- const runtime = getAuthRuntime();
1385
- if (runtime && runtime.urlProvider.getCurrentUrl().search) cleanUrlSoft();
1386
- else if (typeof window !== "undefined" && window.location.search) cleanUrlSoft();
1387
- clearAllAuthFlags();
1388
- clearProcessing();
1389
- return false;
1390
- }
1391
1625
  const storedTenantKey = getTenantKey();
1392
- let currentTenantKey = null;
1393
- try {
1394
- const propsConfigStr = getStorage(getStorageKey("props_config"));
1395
- if (propsConfigStr) {
1396
- const propsConfig = JSON.parse(propsConfigStr);
1397
- currentTenantKey = propsConfig.tenantKey || null;
1398
- }
1399
- } catch {
1400
- }
1401
- if (!currentTenantKey) currentTenantKey = storedTenantKey;
1626
+ const currentTenantKey = resolveTenantKey() ?? storedTenantKey;
1402
1627
  if (currentTenantKey && storedTenantKey && currentTenantKey !== storedTenantKey) {
1403
- logger.info("TenantKey changed - bypassing callback, starting fresh login");
1628
+ logger.warn("[Callback] Early exit: tenant key changed before exchange", {
1629
+ currentTenantKey,
1630
+ storedTenantKey
1631
+ });
1404
1632
  clearAllStorage();
1405
1633
  clearAllAuthFlags();
1406
1634
  clearProcessing();
@@ -1410,7 +1638,10 @@ async function handleCallback() {
1410
1638
  if (isCallbackUrl()) cleanUrlSoft();
1411
1639
  return false;
1412
1640
  }
1413
- if (!isCallbackUrl()) return false;
1641
+ if (!isCallbackUrl()) {
1642
+ logger.warn("[Callback] Early exit: URL is not callback URL");
1643
+ return false;
1644
+ }
1414
1645
  markProcessing();
1415
1646
  clearFlowFlags();
1416
1647
  urlCleaned = false;
@@ -1421,70 +1652,76 @@ async function handleCallback() {
1421
1652
  const state = params.get("state");
1422
1653
  const error = params.get("error");
1423
1654
  if (error || !code || !state) {
1424
- logger.error("Invalid OAuth callback params", { error, code, state });
1655
+ logger.error("[Callback] Early exit: invalid OAuth callback params", { error, code, state });
1425
1656
  clearAllAuthFlags();
1426
1657
  clearProcessing();
1427
- markProcessed();
1428
1658
  cleanUrlSoft();
1429
1659
  return false;
1430
1660
  }
1431
1661
  const storedState = getState();
1432
1662
  if (!storedState || storedState !== state) {
1433
1663
  const currentTenantKeyFromUrl = extractTenantKey();
1434
- let storedTenantKeyFromProps = null;
1435
- try {
1436
- const propsConfigStr = getStorage(getStorageKey("props_config"));
1437
- if (propsConfigStr) {
1438
- const propsConfig = JSON.parse(propsConfigStr);
1439
- storedTenantKeyFromProps = propsConfig.tenantKey || null;
1440
- }
1441
- } catch {
1442
- }
1443
- const tenantKeyChanged = currentTenantKeyFromUrl && storedTenantKeyFromProps && currentTenantKeyFromUrl !== storedTenantKeyFromProps;
1664
+ const resolvedTenantKey = resolveTenantKey();
1665
+ const tenantKeyChanged = currentTenantKeyFromUrl && resolvedTenantKey && currentTenantKeyFromUrl !== resolvedTenantKey;
1444
1666
  if (tenantKeyChanged) {
1445
- logger.info("OAuth state mismatch due to tenantKey change - expected behavior, starting fresh login");
1667
+ logger.warn("[Callback] Early exit: state mismatch due to tenant key change");
1446
1668
  } else {
1447
- logger.error("OAuth state mismatch - redirecting to login");
1669
+ logger.error("[Callback] Early exit: OAuth state mismatch - redirecting to login");
1448
1670
  }
1449
1671
  clearAllAuthFlags();
1450
1672
  clearProcessing();
1451
- markProcessed();
1452
1673
  cleanUrlSoft();
1453
1674
  setStorage(getStorageKey("state_mismatch"), "true");
1454
1675
  return false;
1455
1676
  }
1456
1677
  const codeVerifier = getCodeVerifier();
1457
1678
  if (!codeVerifier) {
1458
- logger.error("Missing PKCE verifier");
1679
+ logger.error("[Callback] Early exit: missing PKCE verifier");
1459
1680
  clearAllAuthFlags();
1460
1681
  clearProcessing();
1461
- markProcessed();
1462
1682
  cleanUrlSoft();
1463
1683
  return false;
1464
1684
  }
1465
- const clientId = getClientId();
1466
- const redirectUri = getRedirectUri();
1467
- const tenantId = getTenantId();
1685
+ let clientId = getClientId();
1686
+ let redirectUri = getRedirectUri();
1687
+ let tenantId = getTenantId();
1688
+ if (!clientId || !redirectUri || !tenantId) {
1689
+ logger.warn("[Callback] OAuth config incomplete \u2014 attempting tenant re-resolve", {
1690
+ hasClientId: !!clientId,
1691
+ hasRedirectUri: !!redirectUri,
1692
+ hasTenantId: !!tenantId
1693
+ });
1694
+ const recovered = await tryRecoverOAuthConfigFromTenant();
1695
+ if (recovered) {
1696
+ if (!redirectUri) redirectUri = recovered.redirectUri;
1697
+ if (!tenantId) tenantId = recovered.tenantId;
1698
+ clientId = getClientId() ?? normalizeClientIdForOAuthTokenBody(recovered.clientId) ?? null;
1699
+ }
1700
+ }
1468
1701
  if (!clientId || !redirectUri || !tenantId) {
1469
- logger.error("Missing OAuth config during callback");
1702
+ logger.error("[Callback] Early exit: missing OAuth config during callback", {
1703
+ hasClientId: !!clientId,
1704
+ hasRedirectUri: !!redirectUri,
1705
+ hasTenantId: !!tenantId
1706
+ });
1470
1707
  clearAllAuthFlags();
1471
1708
  clearProcessing();
1472
- markProcessed();
1473
1709
  cleanUrlSoft();
1474
1710
  return false;
1475
1711
  }
1476
1712
  const existingToken = getAccessToken();
1477
1713
  if (existingToken && !isAccessTokenExpired()) {
1478
- logger.debug("Valid token already exists - skipping code exchange");
1714
+ logger.warn("[Callback] Early exit: valid token already exists - skipping code exchange");
1479
1715
  removeStorage(STORAGE_KEYS.codeVerifier);
1480
1716
  removeStorage(STORAGE_KEYS.state);
1481
1717
  clearAllAuthFlags();
1482
1718
  markProcessed();
1483
- cleanUrl();
1719
+ cleanUrlSoft();
1484
1720
  resetRedirectLoopGuard();
1485
1721
  emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
1486
1722
  return true;
1487
1723
  }
1724
+ logger.info("[Callback] Entering token exchange");
1488
1725
  const tokenData = await exchangeCodeForTokens({
1489
1726
  code,
1490
1727
  codeVerifier,
@@ -1504,7 +1741,7 @@ async function handleCallback() {
1504
1741
  removeStorage(STORAGE_KEYS.state);
1505
1742
  clearAllAuthFlags();
1506
1743
  markProcessed();
1507
- cleanUrl();
1744
+ cleanUrlSoft();
1508
1745
  logger.info("OAuth callback processed successfully");
1509
1746
  resetRedirectLoopGuard();
1510
1747
  emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
@@ -1565,157 +1802,6 @@ async function handleCallback() {
1565
1802
  }
1566
1803
  }
1567
1804
 
1568
- // src/infrastructure/tenant/TenantConfigCache.ts
1569
- var DEFAULT_TTL_MS = 5 * 60 * 1e3;
1570
- var TenantConfigCache = class {
1571
- constructor(ttlMs = DEFAULT_TTL_MS) {
1572
- this.cache = /* @__PURE__ */ new Map();
1573
- this.inFlight = /* @__PURE__ */ new Map();
1574
- this.ttlMs = ttlMs;
1575
- }
1576
- get(tenantKey) {
1577
- const entry = this.cache.get(tenantKey);
1578
- if (!entry) return null;
1579
- if (Date.now() >= entry.expiresAt) {
1580
- this.cache.delete(tenantKey);
1581
- return null;
1582
- }
1583
- return entry.config;
1584
- }
1585
- set(tenantKey, config) {
1586
- this.cache.set(tenantKey, {
1587
- config,
1588
- expiresAt: Date.now() + this.ttlMs
1589
- });
1590
- }
1591
- invalidate(tenantKey) {
1592
- if (tenantKey) {
1593
- this.cache.delete(tenantKey);
1594
- this.inFlight.delete(tenantKey);
1595
- } else {
1596
- this.cache.clear();
1597
- this.inFlight.clear();
1598
- }
1599
- }
1600
- async getOrResolve(tenantKey, resolver) {
1601
- if (!tenantKey || !tenantKey.trim()) return null;
1602
- const key = tenantKey.trim();
1603
- const cached = this.get(key);
1604
- if (cached) {
1605
- logger.debug("Tenant config from cache", { tenantKey: key });
1606
- return cached;
1607
- }
1608
- const existing = this.inFlight.get(key);
1609
- if (existing) {
1610
- logger.debug("Tenant resolution reused (in-flight)", { tenantKey: key });
1611
- return existing;
1612
- }
1613
- const promise = (async () => {
1614
- try {
1615
- const config = await resolver(key);
1616
- if (config) this.set(key, config);
1617
- return config;
1618
- } finally {
1619
- this.inFlight.delete(key);
1620
- }
1621
- })();
1622
- this.inFlight.set(key, promise);
1623
- return promise;
1624
- }
1625
- };
1626
-
1627
- // src/infrastructure/tenant/tenantResolution.ts
1628
- var defaultTenantConfigCache = new TenantConfigCache();
1629
- function getTenantKeyFromRuntime(url, config) {
1630
- try {
1631
- const propsConfigStr = getStorage(getStorageKey("props_config"));
1632
- if (propsConfigStr) {
1633
- const propsConfig = JSON.parse(propsConfigStr);
1634
- if (propsConfig.tenantKey?.trim()) return propsConfig.tenantKey.trim();
1635
- return null;
1636
- }
1637
- } catch {
1638
- }
1639
- return extractTenantKeyFromUrl(url, config, getStorage, getStorageKey);
1640
- }
1641
- function getPropsConfig() {
1642
- try {
1643
- const propsConfigStr = getStorage(getStorageKey("props_config"));
1644
- if (!propsConfigStr) return null;
1645
- const parsed = JSON.parse(propsConfigStr);
1646
- if (parsed && typeof parsed === "object" && "authServerUrl" in parsed) {
1647
- delete parsed.authServerUrl;
1648
- }
1649
- return parsed;
1650
- } catch {
1651
- removeStorage(getStorageKey("props_config"));
1652
- return null;
1653
- }
1654
- }
1655
- function mergeConfigWithProps(apiEnvConfig, propsConfig, tenantKey) {
1656
- return {
1657
- tenantId: propsConfig.tenantId || apiEnvConfig.tenantId,
1658
- authServer: apiEnvConfig.authServer,
1659
- tenantKey: tenantKey || apiEnvConfig.tenantKey,
1660
- client: {
1661
- clientId: propsConfig.clientId || apiEnvConfig.client.clientId,
1662
- redirectUri: apiEnvConfig.client.redirectUri,
1663
- scopes: apiEnvConfig.client.scopes
1664
- }
1665
- };
1666
- }
1667
- async function resolveTenantConfig(tenantKey, tenantResolver) {
1668
- if (!tenantKey) return null;
1669
- const propsConfig = getPropsConfig();
1670
- const resolved = await tenantResolver.resolve(tenantKey);
1671
- if (!resolved) return null;
1672
- removeStorage(getStorageKey("props_config"));
1673
- return {
1674
- ...propsConfig ? mergeConfigWithProps(resolved, propsConfig, tenantKey) : resolved,
1675
- tenantKey: tenantKey || resolved.tenantKey
1676
- };
1677
- }
1678
- async function resolveTenantConfigCached(tenantKey, tenantResolver) {
1679
- if (!tenantKey) return null;
1680
- return defaultTenantConfigCache.getOrResolve(
1681
- tenantKey,
1682
- (key) => resolveTenantConfig(key, tenantResolver)
1683
- );
1684
- }
1685
-
1686
- // src/application/flows/ForceLoginFlow.ts
1687
- var ForceLoginFlow = class {
1688
- async execute(context) {
1689
- const { runtime } = context;
1690
- const { urlProvider, logger: log } = runtime;
1691
- const getUrl = () => urlProvider.getCurrentUrl();
1692
- if (isCallbackUrlFromUrl(getUrl())) {
1693
- await handleCallback();
1694
- if (isCallbackUrlFromUrl(getUrl())) {
1695
- log.warn("Callback URL still present after cleanup, forcing URL clean");
1696
- const u = getUrl();
1697
- urlProvider.redirect(`${u.origin}${u.pathname}`);
1698
- return { completed: true };
1699
- }
1700
- }
1701
- clearFlowFlags();
1702
- const tenantKey = getTenantKeyFromRuntime(getUrl(), runtime.configProvider);
1703
- if (!tenantKey) return { completed: true };
1704
- const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
1705
- if (!tenantConfig) {
1706
- log.error("Tenant config resolution failed for force login", { tenantKey });
1707
- throw new SanitizedError(
1708
- "TENANT_CONFIG_RESOLUTION_FAILED",
1709
- "Unable to resolve tenant configuration",
1710
- new Error(`Tenant config resolution failed for tenantKey: ${tenantKey}`)
1711
- );
1712
- }
1713
- storeTenantConfig(tenantConfig);
1714
- await initiateAuthFlow(tenantConfig);
1715
- return { completed: true };
1716
- }
1717
- };
1718
-
1719
1805
  // src/application/flows/CallbackFlow.ts
1720
1806
  var CallbackFlow = class {
1721
1807
  async execute(context) {
@@ -1803,21 +1889,16 @@ var RefreshFlow = class {
1803
1889
  error: error instanceof Error ? error.message : String(error)
1804
1890
  });
1805
1891
  }
1806
- const tenantKeyFromRuntime = getTenantKeyFromRuntime(getUrl(), runtime.configProvider);
1807
- const tenantKeyFromStorage = getStorage(getStorageKey("tenant_key"));
1808
- const tenantKeyFromConfig = runtime.configProvider.getClientCode?.() ?? "";
1809
- const tenantKey = (tenantKeyFromRuntime || tenantKeyFromStorage || tenantKeyFromConfig)?.trim();
1892
+ const tenantKey = resolveTenantKey();
1810
1893
  log.debug("[RefreshFlow] Tenant key resolved", {
1811
1894
  tenantKey: tenantKey ?? "(null)",
1812
- fromRuntime: tenantKeyFromRuntime ?? "(null)",
1813
- fromStorage: tenantKeyFromStorage ?? "(null)",
1814
- fromConfig: tenantKeyFromConfig || "(null)"
1895
+ source: tenantKey ? "resolved" : "none"
1815
1896
  });
1816
1897
  if (!tenantKey) {
1817
1898
  throw new SanitizedError(
1818
1899
  "TENANT_KEY_RESOLUTION_FAILED",
1819
1900
  "Unable to resolve tenant",
1820
- new Error("Tenant key missing")
1901
+ new Error(`Tenant key missing for refresh flow at url=${getUrl().href}`)
1821
1902
  );
1822
1903
  }
1823
1904
  const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
@@ -1846,22 +1927,17 @@ var TenantSwitchFlow = class {
1846
1927
  emitAuthEvent(AuthEventNames.TENANT_SWITCHED, { tenantKey: newTenantKey ?? void 0 });
1847
1928
  removeStorage(getStorageKey("tenant_switched"));
1848
1929
  removeStorage(getStorageKey("new_tenant_key"));
1849
- const tenantKeyFromRuntime = getTenantKeyFromRuntime(getUrl(), runtime.configProvider);
1850
- const tenantKeyFromStorage = getStorage(getStorageKey("tenant_key"));
1851
- const tenantKeyFromConfig = runtime.configProvider.getClientCode?.() ?? "";
1852
- const tenantKey = (newTenantKey || tenantKeyFromRuntime || tenantKeyFromStorage || tenantKeyFromConfig)?.trim();
1930
+ const tenantKey = (newTenantKey || resolveTenantKey())?.trim() || null;
1853
1931
  log.debug("[TenantSwitchFlow] Tenant key resolved", {
1854
1932
  tenantKey: tenantKey ?? "(null)",
1855
1933
  fromNewTenantKey: newTenantKey ?? "(null)",
1856
- fromRuntime: tenantKeyFromRuntime ?? "(null)",
1857
- fromStorage: tenantKeyFromStorage ?? "(null)",
1858
- fromConfig: tenantKeyFromConfig || "(null)"
1934
+ source: newTenantKey ? "new_tenant_key" : tenantKey ? "resolved" : "none"
1859
1935
  });
1860
1936
  if (!tenantKey) {
1861
1937
  throw new SanitizedError(
1862
1938
  "TENANT_KEY_RESOLUTION_FAILED",
1863
1939
  "Unable to resolve tenant after switch",
1864
- new Error("Tenant key missing after switch")
1940
+ new Error(`Tenant key missing after switch at url=${getUrl().href}`)
1865
1941
  );
1866
1942
  }
1867
1943
  const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
@@ -1872,6 +1948,21 @@ var TenantSwitchFlow = class {
1872
1948
  new Error(`Tenant config resolution failed for tenantKey: ${tenantKey}`)
1873
1949
  );
1874
1950
  }
1951
+ try {
1952
+ const cfg = getAuthConfig2();
1953
+ writeHostConfigCache({
1954
+ apiBaseUrl: cfg.apiBaseUrl,
1955
+ authServerUrl: cfg.authServerUrl,
1956
+ callbackUrl: cfg.callbackUrl,
1957
+ appId: cfg.appId,
1958
+ clientCode: tenantConfig.client?.clientCode ?? cfg.clientCode ?? "",
1959
+ clientId: cfg.clientId || tenantConfig.client?.clientId,
1960
+ tenantId: tenantConfig.tenantId ?? cfg.tenantId,
1961
+ tenantKey,
1962
+ tenantCode: tenantKey
1963
+ });
1964
+ } catch {
1965
+ }
1875
1966
  setTenantKey(tenantKey);
1876
1967
  storeTenantConfig(tenantConfig);
1877
1968
  await initiateAuthFlow(tenantConfig);
@@ -1896,21 +1987,16 @@ var LoginFlow = class {
1896
1987
  }
1897
1988
  const wasStateMismatch = getStorage(getStorageKey("state_mismatch")) === "true";
1898
1989
  if (wasStateMismatch) removeStorage(getStorageKey("state_mismatch"));
1899
- const tenantKeyFromRuntime = getTenantKeyFromRuntime(url, runtime.configProvider);
1900
- const tenantKeyFromStorage = getStorage(getStorageKey("tenant_key"));
1901
- const tenantKeyFromConfig = runtime.configProvider.getClientCode?.() ?? "";
1902
- const tenantKey = (tenantKeyFromRuntime || tenantKeyFromStorage || tenantKeyFromConfig)?.trim();
1990
+ const tenantKey = resolveTenantKey();
1903
1991
  log.debug("[LoginFlow] Tenant key resolved", {
1904
1992
  tenantKey: tenantKey ?? "(null)",
1905
- fromRuntime: tenantKeyFromRuntime ?? "(null)",
1906
- fromStorage: tenantKeyFromStorage ?? "(null)",
1907
- fromConfig: tenantKeyFromConfig || "(null)"
1993
+ source: tenantKey ? "resolved" : "none"
1908
1994
  });
1909
1995
  if (!tenantKey) {
1910
1996
  throw new SanitizedError(
1911
1997
  "TENANT_KEY_RESOLUTION_FAILED",
1912
1998
  "Unable to resolve tenant",
1913
- new Error("Tenant key missing")
1999
+ new Error(`Tenant key missing for login flow at url=${url.href}`)
1914
2000
  );
1915
2001
  }
1916
2002
  log.debug("[LoginFlow] Resolving tenant config", { tenantKey });
@@ -1939,7 +2025,6 @@ var LoginFlow = class {
1939
2025
  };
1940
2026
 
1941
2027
  // src/application/flows/getFlowForState.ts
1942
- var forceLoginFlow = new ForceLoginFlow();
1943
2028
  var callbackFlow = new CallbackFlow();
1944
2029
  var authenticatedFlow = new AuthenticatedFlow();
1945
2030
  var refreshFlow = new RefreshFlow();
@@ -1947,8 +2032,6 @@ var tenantSwitchFlow = new TenantSwitchFlow();
1947
2032
  var loginFlow = new LoginFlow();
1948
2033
  function getFlowForState(state) {
1949
2034
  switch (state) {
1950
- case "FORCE_LOGIN" /* FORCE_LOGIN */:
1951
- return forceLoginFlow;
1952
2035
  case "PROCESSING_CALLBACK" /* PROCESSING_CALLBACK */:
1953
2036
  return callbackFlow;
1954
2037
  case "AUTHENTICATED" /* AUTHENTICATED */:
@@ -2041,6 +2124,22 @@ async function bootstrap() {
2041
2124
  return bootstrapImpl();
2042
2125
  }
2043
2126
 
2127
+ // src/infrastructure/watchers/watcher-guard.ts
2128
+ var suspended = false;
2129
+ var generation = 0;
2130
+ function suspendWatchers() {
2131
+ suspended = true;
2132
+ generation += 1;
2133
+ return generation;
2134
+ }
2135
+ function resumeWatchers(expectedGen) {
2136
+ if (expectedGen !== void 0 && expectedGen !== generation) return;
2137
+ suspended = false;
2138
+ }
2139
+ function areWatchersSuspended() {
2140
+ return suspended;
2141
+ }
2142
+
2044
2143
  // src/strategies/cookie/cookie-auth.ts
2045
2144
  var COOKIE_NAMES = {
2046
2145
  accessToken: "tenneo_auth_access_token",
@@ -2167,36 +2266,14 @@ function checkCookiesDeleted() {
2167
2266
  }
2168
2267
  }
2169
2268
  function handleCookieDeletion() {
2269
+ if (areWatchersSuspended()) {
2270
+ logger.debug("Cookie deletion handler skipped \u2014 watchers suspended (logout/teardown)");
2271
+ return;
2272
+ }
2170
2273
  try {
2171
2274
  logger.info("Cookies deleted detected - clearing session and redirecting to login");
2172
- const forceLoginSet = getStorage(getStorageKey("force_login")) === "true";
2173
- const logoutInProgress = getStorage(getStorageKey("logout_in_progress")) === "true";
2174
- if (forceLoginSet || logoutInProgress) {
2175
- logger.debug(
2176
- "Force login or logout in progress - skipping bootstrap from cookie watcher (avoids duplicate by-code API call)"
2177
- );
2178
- return;
2179
- }
2180
2275
  clearAuthStorage();
2181
- try {
2182
- if (typeof window !== "undefined") {
2183
- const forceLogin = window.localStorage.getItem(getStorageKey("force_login"));
2184
- const logoutInProgressPreserve = window.localStorage.getItem(
2185
- getStorageKey("logout_in_progress")
2186
- );
2187
- clearAuthSessionStorage();
2188
- if (forceLogin === "true")
2189
- window.localStorage.setItem(getStorageKey("force_login"), "true");
2190
- if (logoutInProgressPreserve === "true")
2191
- window.localStorage.setItem(getStorageKey("logout_in_progress"), "true");
2192
- } else {
2193
- clearAuthSessionStorage();
2194
- }
2195
- } catch (error) {
2196
- logger.warn("Failed to clear sessionStorage", {
2197
- error: error instanceof Error ? error.message : String(error)
2198
- });
2199
- }
2276
+ clearAuthSessionStorage();
2200
2277
  const cb = getSessionClearedCallback();
2201
2278
  if (cb) {
2202
2279
  Promise.resolve(cb()).catch((error) => {
@@ -2230,13 +2307,7 @@ function startCookieWatcher() {
2230
2307
  allCookies: initialCookies
2231
2308
  });
2232
2309
  watchInterval = setInterval(async () => {
2233
- const logoutInProgress = typeof window !== "undefined" && window.localStorage.getItem(getStorageKey("logout_in_progress")) === "true";
2234
- if (logoutInProgress) {
2235
- logger.debug("Logout detected. Cookie watcher will not trigger bootstrap.");
2236
- lastCookieSnapshot = getCookieSnapshot();
2237
- lastCookieMap = getCookieMap();
2238
- return;
2239
- }
2310
+ if (areWatchersSuspended()) return;
2240
2311
  const cookiesDeleted = checkCookiesDeleted();
2241
2312
  if (cookiesDeleted) {
2242
2313
  logger.info("Cookies deleted detected by cookie watcher");
@@ -2300,9 +2371,11 @@ function checkStorageCleared() {
2300
2371
  }
2301
2372
  }
2302
2373
  function handleStorageEvent(event) {
2374
+ if (areWatchersSuspended()) return;
2303
2375
  if (typeof window !== "undefined" && event.storageArea === window.sessionStorage) {
2304
2376
  const wasCleared = checkStorageCleared();
2305
2377
  if (wasCleared) {
2378
+ if (areWatchersSuspended()) return;
2306
2379
  logger.info("Detected manual sessionStorage clear! Redirecting to login...");
2307
2380
  const cb = getSessionClearedCallback();
2308
2381
  if (cb) {
@@ -2322,13 +2395,10 @@ function startStorageWatcher() {
2322
2395
  storageEventListener = handleStorageEvent;
2323
2396
  window.addEventListener("storage", storageEventListener);
2324
2397
  const pollInterval = setInterval(() => {
2325
- const logoutInProgress = typeof window !== "undefined" && window.localStorage.getItem(getStorageKey("logout_in_progress")) === "true";
2326
- if (logoutInProgress) {
2327
- logger.debug("Logout detected. Storage watcher will not trigger bootstrap.");
2328
- return;
2329
- }
2398
+ if (areWatchersSuspended()) return;
2330
2399
  const wasCleared = checkStorageCleared();
2331
2400
  if (wasCleared) {
2401
+ if (areWatchersSuspended()) return;
2332
2402
  logger.info("Detected manual sessionStorage clear (polling)! Redirecting to login...");
2333
2403
  clearInterval(pollInterval);
2334
2404
  const cb = getSessionClearedCallback();
@@ -2413,62 +2483,68 @@ function getCsrfToken() {
2413
2483
  }
2414
2484
  async function callServerLogout(logoutUrl) {
2415
2485
  const csrfToken = getCsrfToken();
2416
- async function doRequest(method) {
2417
- const isPost = method === "POST";
2418
- const headers = {
2419
- "X-Requested-With": "XMLHttpRequest",
2420
- Accept: "application/json, */*"
2421
- };
2422
- if (isPost) {
2423
- headers["Content-Type"] = "application/x-www-form-urlencoded";
2424
- }
2425
- if (csrfToken) {
2426
- headers["X-CSRF-Token"] = csrfToken;
2427
- }
2486
+ const headers = {
2487
+ "X-Requested-With": "XMLHttpRequest",
2488
+ Accept: "application/json, */*",
2489
+ "Content-Type": "application/x-www-form-urlencoded"
2490
+ };
2491
+ if (csrfToken) {
2492
+ headers["X-CSRF-Token"] = csrfToken;
2493
+ }
2494
+ try {
2428
2495
  const response = await fetch(logoutUrl, {
2429
- method,
2496
+ method: "POST",
2430
2497
  mode: "cors",
2431
2498
  headers,
2432
- credentials: "include"
2499
+ credentials: "include",
2500
+ body: ""
2433
2501
  });
2434
2502
  if (!response.ok) {
2435
- logger.warn("Server logout HTTP error", {
2436
- method,
2503
+ logger.warn("[logout] Server logout HTTP error", {
2437
2504
  status: response.status,
2438
2505
  statusText: response.statusText
2439
2506
  });
2440
2507
  return { success: false };
2441
2508
  }
2442
- const data = await response.json().catch(() => ({}));
2443
- logger.info(`Server logout response (${method})`, { response: data });
2509
+ const contentType = response.headers.get("content-type") || "";
2510
+ if (!contentType.includes("application/json")) {
2511
+ logger.info("[logout] Server logout: OK without JSON body", {
2512
+ status: response.status
2513
+ });
2514
+ return { success: true, response: { success: true } };
2515
+ }
2516
+ const text = await response.text();
2517
+ let data = {};
2518
+ if (text?.trim()) {
2519
+ try {
2520
+ data = JSON.parse(text);
2521
+ } catch {
2522
+ logger.warn("[logout] Server returned non-JSON body with JSON content-type");
2523
+ return { success: false };
2524
+ }
2525
+ }
2526
+ logger.info("[logout] Server logout response", { response: data });
2444
2527
  if (data && (data.success === true || data.logout === true)) {
2445
2528
  return { success: true, response: data };
2446
2529
  }
2447
- logger.warn("Server logout response invalid payload", {
2448
- method,
2449
- response: data
2450
- });
2530
+ if (!text || text.trim() === "") {
2531
+ return { success: true, response: { success: true } };
2532
+ }
2533
+ logger.warn("[logout] Server logout: unexpected JSON payload", { response: data });
2451
2534
  return { success: false, response: data };
2452
- }
2453
- try {
2454
- const postResult = await doRequest("POST");
2455
- if (postResult.success) return postResult;
2456
- logger.debug("POST logout did not succeed, trying GET");
2457
- const getResult = await doRequest("GET");
2458
- return getResult;
2459
2535
  } catch (error) {
2460
- logger.warn("Server logout failed (both POST and GET)", {
2536
+ logger.warn("[logout] Server logout request failed", {
2461
2537
  error: error instanceof Error ? error.message : String(error)
2462
2538
  });
2463
2539
  return { success: false };
2464
2540
  }
2465
2541
  }
2466
2542
  function performLocalLogoutCleanup() {
2467
- logger.debug("Performing local logout cleanup");
2543
+ logger.debug("[logout] Local cleanup: cookies + auth storage");
2468
2544
  try {
2469
2545
  getAuthRuntime()?.clearCookies?.clearCookies();
2470
2546
  } catch (error) {
2471
- logger.debug("Runtime cookie clearer failed (ignored)", {
2547
+ logger.debug("[logout] Runtime cookie clearer failed (ignored)", {
2472
2548
  error: error instanceof Error ? error.message : String(error)
2473
2549
  });
2474
2550
  }
@@ -2476,11 +2552,11 @@ function performLocalLogoutCleanup() {
2476
2552
  try {
2477
2553
  clearAuthStorage();
2478
2554
  } catch (error) {
2479
- logger.warn("clearAuthStorage failed (ignored)", {
2555
+ logger.warn("[logout] clearAuthStorage failed", {
2480
2556
  error: error instanceof Error ? error.message : String(error)
2481
2557
  });
2482
2558
  }
2483
- logger.info("Local logout cleanup completed");
2559
+ logger.info("[logout] Local cleanup completed");
2484
2560
  }
2485
2561
  function acquireLogoutLock() {
2486
2562
  const LOCK_KEY = getStorageKey("logout_lock");
@@ -2489,7 +2565,7 @@ function acquireLogoutLock() {
2489
2565
  if (existingLock) {
2490
2566
  const lockTime = Number(existingLock);
2491
2567
  if (!Number.isNaN(lockTime) && now - lockTime < 5e3) {
2492
- logger.debug("Logout lock active \u2013 skipping duplicate logout", {
2568
+ logger.debug("[logout] Lock active \u2014 duplicate tab logout skipped", {
2493
2569
  now,
2494
2570
  lockTime
2495
2571
  });
@@ -2505,102 +2581,95 @@ function releaseLogoutLock() {
2505
2581
  } catch {
2506
2582
  }
2507
2583
  }
2508
- async function logout(tenantKey) {
2509
- setStorage(getStorageKey("force_login"), "true");
2510
- if (tenantKey?.trim()) {
2511
- setStorage(getStorageKey("preserved_tenant_key"), tenantKey.trim());
2512
- }
2584
+ var logoutInFlight = null;
2585
+ async function runLogoutPipeline(tenantKey) {
2586
+ const gen = suspendWatchers();
2587
+ logger.info("[logout] Lifecycle: suspend watchers", { generation: gen });
2588
+ stopStorageWatcher();
2589
+ stopCookieWatcher();
2513
2590
  if (!acquireLogoutLock()) {
2514
- return { success: true, message: "Logout already in progress." };
2591
+ resumeWatchers(gen);
2592
+ logger.debug("[logout] Lifecycle: resume (skipped \u2014 lock)");
2593
+ return { success: true, message: "Logout already in progress in another tab." };
2515
2594
  }
2595
+ let serverLogoutSuccess = false;
2596
+ let serverResponse;
2516
2597
  try {
2517
- stopStorageWatcher();
2518
- stopCookieWatcher();
2519
- const apiAuthBaseUrl = Config.getAuthServerUrl();
2520
- let serverLogoutSuccess = false;
2521
- let serverResponse;
2598
+ if (tenantKey?.trim()) {
2599
+ applyRuntimeTenantSwitch(tenantKey.trim(), { syncStorageCache: true });
2600
+ logger.info("[logout] Runtime tenant applied for logout context", {
2601
+ tenantKey: tenantKey.trim()
2602
+ });
2603
+ }
2604
+ const authConfig = getAuthConfig2();
2605
+ const apiAuthBaseUrl = authConfig.authServerUrl;
2522
2606
  if (apiAuthBaseUrl) {
2523
2607
  const logoutUrl = buildUrl(apiAuthBaseUrl, "account/logout");
2524
- logger.info("Calling auth server logout", { logoutUrl });
2608
+ logger.info("[logout] Calling auth server logout (POST)", { logoutUrl });
2525
2609
  const logoutResult = await callServerLogout(logoutUrl);
2526
2610
  serverLogoutSuccess = logoutResult.success;
2527
2611
  serverResponse = logoutResult.response;
2528
2612
  if (!serverLogoutSuccess) {
2529
- logger.warn("Server logout failed \u2013 session may still exist", {
2613
+ logger.warn("[logout] Server logout did not confirm success \u2014 clearing local session anyway", {
2530
2614
  logoutUrl,
2531
2615
  serverResponse
2532
2616
  });
2533
2617
  }
2534
2618
  } else {
2535
- logger.warn("API auth base URL not configured, performing local logout only");
2619
+ logger.warn("[logout] authServerUrl missing \u2014 local logout only");
2536
2620
  }
2621
+ } catch (error) {
2622
+ logger.error("[logout] Unexpected error before local cleanup", {
2623
+ error: error instanceof Error ? error.message : String(error)
2624
+ });
2625
+ }
2626
+ try {
2537
2627
  performLocalLogoutCleanup();
2538
- const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
2539
- if (preservedTenantKey) {
2540
- const propsKey = getStorageKey("props_config");
2541
- let existing = {};
2542
- try {
2543
- const raw = getStorage(propsKey);
2544
- if (raw) {
2545
- existing = JSON.parse(raw);
2546
- }
2547
- } catch {
2548
- existing = {};
2549
- }
2550
- const merged = {
2551
- ...existing,
2552
- tenantKey: preservedTenantKey
2553
- };
2554
- setStorage(propsKey, JSON.stringify(merged));
2555
- removeStorage(getStorageKey("preserved_tenant_key"));
2556
- }
2557
- resetCookieWatcher();
2558
- startStorageWatcher();
2559
- releaseLogoutLock();
2560
- logger.info("Logout completed \u2013 flags set, storage cleared, fresh login required", {
2561
- serverLogoutSuccess,
2562
- serverResponse
2628
+ } catch (error) {
2629
+ logger.error("[logout] Local cleanup threw", {
2630
+ error: error instanceof Error ? error.message : String(error)
2563
2631
  });
2564
- emitAuthEvent(AuthEventNames.LOGOUT_COMPLETED, {
2565
- serverLogoutSuccess,
2566
- tenantKey: getStorage(getStorageKey("preserved_tenant_key")) || void 0,
2567
- message: serverLogoutSuccess ? "Server logout success" : "Local logout only"
2632
+ }
2633
+ try {
2634
+ syncHostConfigCacheFromMemory();
2635
+ logger.debug("[logout] host_config cache refreshed from runtime (not restored from snapshot)");
2636
+ } catch (error) {
2637
+ logger.warn("[logout] Failed to sync host config cache", {
2638
+ error: error instanceof Error ? error.message : String(error)
2568
2639
  });
2569
- return {
2570
- success: true,
2571
- message: serverLogoutSuccess ? serverResponse?.message || "Logout successful. Session cookies cleared. Fresh login required." : "Logged out locally. Fresh login required (server logout may have failed).",
2572
- serverResponse
2573
- };
2640
+ }
2641
+ try {
2642
+ resetCookieWatcher();
2643
+ startStorageWatcher();
2644
+ logger.info("[logout] Watchers restarted");
2574
2645
  } catch (error) {
2575
- logger.error("Logout failed unexpectedly", {
2646
+ logger.warn("[logout] Watcher restart failed", {
2576
2647
  error: error instanceof Error ? error.message : String(error)
2577
2648
  });
2578
- performLocalLogoutCleanup();
2579
- const preservedTenantKey = getStorage(getStorageKey("preserved_tenant_key"));
2580
- if (preservedTenantKey) {
2581
- const propsKey = getStorageKey("props_config");
2582
- let existing = {};
2583
- try {
2584
- const raw = getStorage(propsKey);
2585
- if (raw) {
2586
- existing = JSON.parse(raw);
2587
- }
2588
- } catch {
2589
- existing = {};
2590
- }
2591
- const merged = {
2592
- ...existing,
2593
- tenantKey: preservedTenantKey
2594
- };
2595
- setStorage(propsKey, JSON.stringify(merged));
2596
- removeStorage(getStorageKey("preserved_tenant_key"));
2597
- }
2598
- releaseLogoutLock();
2599
- return {
2600
- success: true,
2601
- message: "Logged out locally due to an error. Fresh login required."
2602
- };
2603
2649
  }
2650
+ releaseLogoutLock();
2651
+ resumeWatchers(gen);
2652
+ logger.info("[logout] Lifecycle: complete", { serverLogoutSuccess });
2653
+ emitAuthEvent(AuthEventNames.LOGOUT_COMPLETED, {
2654
+ serverLogoutSuccess,
2655
+ tenantKey: tenantKey?.trim() || getAuthConfig2().tenantCode || void 0,
2656
+ message: serverLogoutSuccess ? "Server logout success" : "Local logout (server may be pending)"
2657
+ });
2658
+ return {
2659
+ success: true,
2660
+ message: serverLogoutSuccess ? serverResponse?.message || "Logout successful. Fresh login required." : "Logged out locally. Fresh login required (server logout may have failed).",
2661
+ serverResponse
2662
+ };
2663
+ }
2664
+ async function logout(tenantKey) {
2665
+ if (logoutInFlight) {
2666
+ logger.debug("[logout] Coalescing with in-flight logout");
2667
+ return logoutInFlight;
2668
+ }
2669
+ logoutInFlight = runLogoutPipeline(tenantKey).finally(() => {
2670
+ logoutInFlight = null;
2671
+ });
2672
+ return logoutInFlight;
2604
2673
  }
2605
2674
  var AuthContext = createContext(void 0);
2606
2675
  function useAuthContext() {
@@ -2675,7 +2744,6 @@ async function resolveTenant(tenantKey) {
2675
2744
  scopes: DEFAULT_SCOPES2
2676
2745
  }
2677
2746
  };
2678
- console.log("config====", config);
2679
2747
  logger.debug("Resolved tenant config", { tenantCode });
2680
2748
  return config;
2681
2749
  } catch (error) {
@@ -2936,27 +3004,29 @@ function AuthProvider({
2936
3004
  logger.warn("[AuthProvider] callbackUrl missing from host props");
2937
3005
  }
2938
3006
  setConfig({
2939
- ...mergedTenantCode ? { tenantCode: mergedTenantCode } : {},
3007
+ ...mergedTenantCode ? { tenantCode: mergedTenantCode, tenantKey: mergedTenantCode } : {},
2940
3008
  ...mergedApiBaseUrl ? { apiBaseUrl: mergedApiBaseUrl } : {},
2941
3009
  ...mergedAuthServerUrl ? { authServerUrl: mergedAuthServerUrl } : {},
2942
3010
  ...mergedCallbackUrl ? { callbackUrl: mergedCallbackUrl } : {}
2943
3011
  });
3012
+ logConfigResolutionDebug("AuthProvider host props applied");
2944
3013
  return () => {
2945
3014
  clearConfigOverrides();
2946
3015
  };
2947
3016
  }, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
2948
3017
  useEffect(() => {
2949
3018
  try {
2950
- const payload = {
3019
+ writeHostConfigCache({
2951
3020
  tenantCode: mergedTenantCode ?? "",
3021
+ tenantKey: mergedTenantCode ?? "",
3022
+ appId: appId ?? "",
2952
3023
  apiBaseUrl: mergedApiBaseUrl ?? "",
2953
3024
  authServerUrl: mergedAuthServerUrl ?? "",
2954
3025
  callbackUrl: mergedCallbackUrl ?? ""
2955
- };
2956
- setStorage(getStorageKey("host_config"), JSON.stringify(payload));
3026
+ });
2957
3027
  } catch {
2958
3028
  }
2959
- }, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
3029
+ }, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl, appId]);
2960
3030
  useLayoutEffect(() => {
2961
3031
  setStorageKeyPrefix(resolvedTenantKey ?? "");
2962
3032
  const adapter = storageAdapter !== void 0 && storageAdapter !== null ? storageAdapter : createSecureSessionStorageAdapter(namespace);
@@ -2997,24 +3067,18 @@ function AuthProvider({
2997
3067
  const currentUrl = window.location.href;
2998
3068
  const isCallback = currentUrl.includes("code=") || currentUrl.includes("state=");
2999
3069
  if (isCallback) {
3000
- const cleanUrl2 = window.location.origin + window.location.pathname;
3001
- window.history.replaceState({}, document.title, cleanUrl2);
3070
+ const cleanUrl = window.location.origin + window.location.pathname;
3071
+ window.history.replaceState({}, document.title, cleanUrl);
3002
3072
  }
3003
3073
  }
3004
3074
  removeStorage(getStorageKey("access_token"));
3005
3075
  removeStorage(getStorageKey("refresh_token"));
3006
3076
  removeStorage(getStorageKey("expires_at"));
3007
- removeStorage(getStorageKey("id_token"));
3008
3077
  removeStorage(getStorageKey("state"));
3009
3078
  removeStorage(getStorageKey("code_verifier"));
3010
3079
  removeStorage(getStorageKey("callback_processed"));
3011
3080
  clearCallbackProcessing();
3012
3081
  clearFlowFlags();
3013
- removeStorage(getStorageKey("tenant_id"));
3014
- if (typeof window !== "undefined") {
3015
- window.localStorage.removeItem(getStorageKey("force_login"));
3016
- window.localStorage.removeItem(getStorageKey("logout_in_progress"));
3017
- }
3018
3082
  setStorage(getStorageKey("props_changed"), "true");
3019
3083
  setStorage(getStorageKey("tenant_switched"), "true");
3020
3084
  setStorage(getStorageKey("new_tenant_key"), resolvedTenantKey2);
@@ -3027,7 +3091,17 @@ function AuthProvider({
3027
3091
  } else if (previousTenantKey) {
3028
3092
  logger.debug("[AuthProvider] TenantKey unchanged", { tenantKey: resolvedTenantKey2 });
3029
3093
  }
3030
- setStorage(getStorageKey("props_config"), JSON.stringify({ tenantKey: resolvedTenantKey2 }));
3094
+ try {
3095
+ writeHostConfigCache({
3096
+ tenantKey: resolvedTenantKey2,
3097
+ tenantCode: resolvedTenantKey2
3098
+ });
3099
+ logConfigResolutionDebug("AuthProvider tenant key synced to cache", {
3100
+ tenantCode: resolvedTenantKey2,
3101
+ tenantKey: resolvedTenantKey2
3102
+ });
3103
+ } catch {
3104
+ }
3031
3105
  setStorage(getStorageKey("tenant_key"), resolvedTenantKey2);
3032
3106
  } else {
3033
3107
  logger.debug("[AuthProvider] No tenant code determined");
@@ -3226,17 +3300,29 @@ function AuthCallback({
3226
3300
  setStatus("loading");
3227
3301
  setMessage("Completing sign-in...");
3228
3302
  const handled = await handleCallback();
3229
- if (handled) {
3230
- const token = getAccessToken();
3231
- if (token) setTokens(token);
3232
- setStatus("success");
3233
- setMessage("Authentication successful. Redirecting...");
3234
- window.location.replace(successPath);
3235
- } else {
3303
+ if (!handled) {
3236
3304
  setStatus("error");
3237
3305
  setMessage("Authentication failed. Please try again.");
3238
3306
  window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
3307
+ return;
3308
+ }
3309
+ const startedAt = Date.now();
3310
+ const MAX_WAIT_MS = 5e3;
3311
+ while (true) {
3312
+ const token = getAccessToken();
3313
+ if (token) {
3314
+ setTokens(token);
3315
+ setStatus("success");
3316
+ setMessage("Authentication successful. Redirecting...");
3317
+ window.location.replace(successPath);
3318
+ return;
3319
+ }
3320
+ if (Date.now() - startedAt > MAX_WAIT_MS) break;
3321
+ await new Promise((r) => setTimeout(r, 200));
3239
3322
  }
3323
+ setStatus("error");
3324
+ setMessage("Authentication did not complete. Please try again.");
3325
+ window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
3240
3326
  };
3241
3327
  runCallback();
3242
3328
  }, [homePath, loginPath, successPath, setTokens]);
@@ -3417,6 +3503,6 @@ function useAuthInit() {
3417
3503
  );
3418
3504
  }
3419
3505
 
3420
- export { AuthCallback, AuthEventNames, AuthProvider, Config, OAUTH_CONSTANTS, ProtectedRoute, STORAGE_KEYS2 as STORAGE_KEYS, STORAGE_KEY_PREFIX, SanitizedError, bootstrap, buildStorageKey, createAuthEngine, createCookieStorageAdapter, createMemoryStorageAdapter, createOAuth2PkceStrategy, createSecureSessionStorageAdapter, emitAuthEvent, enforceHttps, ensureValidAccessToken, extractTenantKey, getAccessToken2 as getAccessToken, getAuthRuntime, getConfig, getStorageAdapter, getStorageNamespace, getAccessToken as getStoredAccessToken, isAccessTokenExpired, isCallbackUrl, logout, refreshAccessToken, resetCookieWatcher, resolveConfig, resolveTenant, sanitizeErrorMessage, setAuthRuntime, setConfig, setStorageContext, startCookieWatcher, stopCookieWatcher, subscribeAuthEvent, useAuth, useAuthContext, useAuthInit };
3506
+ export { AuthCallback, AuthConfigError, AuthEventNames, AuthProvider, Config, OAUTH_CONSTANTS, ProtectedRoute, STORAGE_KEYS2 as STORAGE_KEYS, STORAGE_KEY_PREFIX, SanitizedError, applyRuntimeTenantSwitch, areWatchersSuspended, bootstrap, buildStorageKey, createAuthEngine, createCookieStorageAdapter, createMemoryStorageAdapter, createOAuth2PkceStrategy, createSecureSessionStorageAdapter, emitAuthEvent, enforceHttps, ensureValidAccessToken, extractTenantKey, getAccessToken2 as getAccessToken, getAuthConfig2 as getAuthConfig, getAuthRuntime, getConfig, getConfigFieldSources, getEffectiveTenantCode, getEffectiveTenantKey, getStorageAdapter, getStorageNamespace, getAccessToken as getStoredAccessToken, initPlugin, isAccessTokenExpired, isCallbackUrl, logConfigResolutionDebug, logout, refreshAccessToken, requireAuthConfig, resetCookieWatcher, resolveConfig, resolveTenant, resumeWatchers, sanitizeErrorMessage, setAuthRuntime, setConfig, setHostConfigProvider, setStorageContext, startCookieWatcher, startStorageWatcher, stopCookieWatcher, stopStorageWatcher, subscribeAuthEvent, suspendWatchers, syncHostConfigCacheFromMemory, useAuth, useAuthContext, useAuthInit, writeHostConfigCache };
3421
3507
  //# sourceMappingURL=index.mjs.map
3422
3508
  //# sourceMappingURL=index.mjs.map