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