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