tenneo-auth-plugin 1.0.2 → 1.0.4
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 +104 -27
- package/dist/index.d.ts +104 -27
- package/dist/index.js +840 -557
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +824 -557
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -367,35 +367,7 @@ function createCookieStorageAdapter(namespace, options) {
|
|
|
367
367
|
};
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
// src/infrastructure/storage/
|
|
371
|
-
var KEY_SUFFIXES = {
|
|
372
|
-
tenantKey: "tenant_key",
|
|
373
|
-
codeVerifier: "code_verifier",
|
|
374
|
-
state: "state",
|
|
375
|
-
accessToken: "access_token",
|
|
376
|
-
refreshToken: "refresh_token",
|
|
377
|
-
expiresAt: "expires_at"
|
|
378
|
-
};
|
|
379
|
-
var STORAGE_KEYS = {
|
|
380
|
-
get tenantKey() {
|
|
381
|
-
return getStorageKey(KEY_SUFFIXES.tenantKey);
|
|
382
|
-
},
|
|
383
|
-
get codeVerifier() {
|
|
384
|
-
return getStorageKey(KEY_SUFFIXES.codeVerifier);
|
|
385
|
-
},
|
|
386
|
-
get state() {
|
|
387
|
-
return getStorageKey(KEY_SUFFIXES.state);
|
|
388
|
-
},
|
|
389
|
-
get accessToken() {
|
|
390
|
-
return getStorageKey(KEY_SUFFIXES.accessToken);
|
|
391
|
-
},
|
|
392
|
-
get refreshToken() {
|
|
393
|
-
return getStorageKey(KEY_SUFFIXES.refreshToken);
|
|
394
|
-
},
|
|
395
|
-
get expiresAt() {
|
|
396
|
-
return getStorageKey(KEY_SUFFIXES.expiresAt);
|
|
397
|
-
}
|
|
398
|
-
};
|
|
370
|
+
// src/infrastructure/storage/kv.ts
|
|
399
371
|
function keyToSuffix(key) {
|
|
400
372
|
const prefix = getStorageKeyPrefix();
|
|
401
373
|
return key.startsWith(prefix) ? key.slice(prefix.length) : key;
|
|
@@ -431,6 +403,36 @@ function removeStorage(key) {
|
|
|
431
403
|
} catch {
|
|
432
404
|
}
|
|
433
405
|
}
|
|
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);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
434
436
|
function clearAuthStorage() {
|
|
435
437
|
const keysToClear = [
|
|
436
438
|
STORAGE_KEYS.accessToken,
|
|
@@ -493,28 +495,6 @@ function isAccessTokenExpired() {
|
|
|
493
495
|
const buffer = 60 * 1e3;
|
|
494
496
|
return Date.now() >= expiresAt - buffer;
|
|
495
497
|
}
|
|
496
|
-
function readHostConfig() {
|
|
497
|
-
try {
|
|
498
|
-
const raw = getStorage(getStorageKey("host_config"));
|
|
499
|
-
if (!raw) return {};
|
|
500
|
-
const parsed = JSON.parse(raw);
|
|
501
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
502
|
-
} catch {
|
|
503
|
-
return {};
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
function getClientId() {
|
|
507
|
-
const value = readHostConfig().clientId;
|
|
508
|
-
return typeof value === "string" && value.trim() ? value : null;
|
|
509
|
-
}
|
|
510
|
-
function getRedirectUri() {
|
|
511
|
-
const value = readHostConfig().redirectUri;
|
|
512
|
-
return typeof value === "string" && value.trim() ? value : null;
|
|
513
|
-
}
|
|
514
|
-
function getTenantId() {
|
|
515
|
-
const value = readHostConfig().tenantId;
|
|
516
|
-
return typeof value === "string" && value.trim() ? value : null;
|
|
517
|
-
}
|
|
518
498
|
function getCodeVerifier() {
|
|
519
499
|
return getStorage(STORAGE_KEYS.codeVerifier);
|
|
520
500
|
}
|
|
@@ -575,6 +555,252 @@ var Logger = class {
|
|
|
575
555
|
};
|
|
576
556
|
var logger = new Logger();
|
|
577
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
|
+
|
|
578
804
|
// src/domain/errors.ts
|
|
579
805
|
function isLocalhostHostname(hostname) {
|
|
580
806
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.startsWith("172.16.");
|
|
@@ -773,71 +999,47 @@ var DEFAULT_TOKEN_GRANT_TYPE = OAUTH_CONSTANTS.TOKEN_GRANT_TYPE;
|
|
|
773
999
|
var DEFAULT_REFRESH_GRANT_TYPE = OAUTH_CONSTANTS.REFRESH_GRANT_TYPE;
|
|
774
1000
|
|
|
775
1001
|
// src/config/index.ts
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
try {
|
|
779
|
-
const raw = getStorage(getStorageKey("host_config"));
|
|
780
|
-
if (!raw) return {};
|
|
781
|
-
const parsed = JSON.parse(raw);
|
|
782
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
783
|
-
} catch {
|
|
784
|
-
return {};
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
function resolveMerged(overrides) {
|
|
788
|
-
const stored = readHostConfigFromStorage();
|
|
789
|
-
const cfg = { ...stored, ...inMemoryConfig, ...overrides ?? {} };
|
|
790
|
-
if (!cfg.tenantCode && cfg.tenantKey) {
|
|
791
|
-
cfg.tenantCode = cfg.tenantKey;
|
|
792
|
-
}
|
|
793
|
-
const trimmed = (v) => v == null ? "" : String(v).trim();
|
|
794
|
-
return {
|
|
795
|
-
apiBaseUrl: trimmed(cfg.apiBaseUrl),
|
|
796
|
-
authServerUrl: trimmed(cfg.authServerUrl),
|
|
797
|
-
callbackUrl: trimmed(cfg.callbackUrl),
|
|
798
|
-
clientCode: trimmed(cfg.clientCode),
|
|
799
|
-
tenantCode: trimmed(cfg.tenantCode),
|
|
800
|
-
appId: trimmed(cfg.appId)
|
|
801
|
-
};
|
|
1002
|
+
function getAuthConfig2(options) {
|
|
1003
|
+
return getAuthConfig(options);
|
|
802
1004
|
}
|
|
803
|
-
function
|
|
804
|
-
return
|
|
1005
|
+
function requireAuthConfig(overrides) {
|
|
1006
|
+
return getAuthConfig({ strict: true, overrides });
|
|
805
1007
|
}
|
|
806
1008
|
function getConfig() {
|
|
807
|
-
return
|
|
808
|
-
}
|
|
809
|
-
function getAuthConfig() {
|
|
810
|
-
return resolveMerged();
|
|
811
|
-
}
|
|
812
|
-
function setConfig(overrides) {
|
|
813
|
-
inMemoryConfig = { ...inMemoryConfig, ...overrides };
|
|
814
|
-
}
|
|
815
|
-
function clearConfigOverrides() {
|
|
816
|
-
inMemoryConfig = {};
|
|
1009
|
+
return resolveConfig();
|
|
817
1010
|
}
|
|
818
1011
|
function getRedirectUri2() {
|
|
819
|
-
const cfg =
|
|
1012
|
+
const cfg = resolveConfig();
|
|
820
1013
|
if (cfg.callbackUrl) return cfg.callbackUrl;
|
|
821
1014
|
if (typeof window !== "undefined") return `${window.location.origin}/callback`;
|
|
822
1015
|
return "";
|
|
823
1016
|
}
|
|
824
1017
|
var Config = {
|
|
825
1018
|
getApiBaseUrl() {
|
|
826
|
-
return
|
|
1019
|
+
return resolveConfig().apiBaseUrl;
|
|
827
1020
|
},
|
|
828
1021
|
getAuthServerUrl() {
|
|
829
|
-
return
|
|
1022
|
+
return resolveConfig().authServerUrl;
|
|
830
1023
|
},
|
|
831
1024
|
getClientCode() {
|
|
832
|
-
return
|
|
1025
|
+
return resolveConfig().clientCode;
|
|
833
1026
|
},
|
|
834
1027
|
getRedirectUri() {
|
|
835
1028
|
return getRedirectUri2();
|
|
836
1029
|
},
|
|
837
1030
|
getAll() {
|
|
838
|
-
return
|
|
1031
|
+
return resolveConfig();
|
|
839
1032
|
}
|
|
840
1033
|
};
|
|
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 });
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
841
1043
|
|
|
842
1044
|
// src/infrastructure/tenant/TenantConfigCache.ts
|
|
843
1045
|
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -913,16 +1115,6 @@ function consumePropsConfig() {
|
|
|
913
1115
|
removeStorage(propsConfigKey);
|
|
914
1116
|
}
|
|
915
1117
|
}
|
|
916
|
-
function readHostConfig2() {
|
|
917
|
-
try {
|
|
918
|
-
const raw = getStorage(getStorageKey("host_config"));
|
|
919
|
-
if (!raw) return null;
|
|
920
|
-
const parsed = JSON.parse(raw);
|
|
921
|
-
return parsed && typeof parsed === "object" ? parsed : null;
|
|
922
|
-
} catch {
|
|
923
|
-
return null;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
1118
|
function validateTenantKey(value) {
|
|
927
1119
|
if (typeof value !== "string") return null;
|
|
928
1120
|
const trimmed = value.trim();
|
|
@@ -931,17 +1123,16 @@ function validateTenantKey(value) {
|
|
|
931
1123
|
function resolveTenantKey() {
|
|
932
1124
|
const propsConfig = consumePropsConfig();
|
|
933
1125
|
const tenantFromProps = validateTenantKey(propsConfig?.tenantKey);
|
|
934
|
-
if (tenantFromProps)
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
logger.warn("Missing host_config while resolving tenant");
|
|
938
|
-
return null;
|
|
1126
|
+
if (tenantFromProps) {
|
|
1127
|
+
logger.debug("[tenant] Resolved tenant from props_config", { tenantKey: tenantFromProps });
|
|
1128
|
+
return tenantFromProps;
|
|
939
1129
|
}
|
|
940
|
-
const
|
|
941
|
-
if (
|
|
942
|
-
|
|
943
|
-
|
|
1130
|
+
const fromHost = validateTenantKey(getEffectiveTenantCode());
|
|
1131
|
+
if (fromHost) {
|
|
1132
|
+
logger.debug("[tenant] Resolved tenant from host/runtime config", { tenantKey: fromHost });
|
|
1133
|
+
return fromHost;
|
|
944
1134
|
}
|
|
1135
|
+
logger.warn("[tenant] Unable to resolve tenant key (props + host/runtime empty)");
|
|
945
1136
|
return null;
|
|
946
1137
|
}
|
|
947
1138
|
async function resolveTenantConfig(tenantKey, tenantResolver) {
|
|
@@ -1074,7 +1265,7 @@ async function refreshAccessToken() {
|
|
|
1074
1265
|
});
|
|
1075
1266
|
throw new Error("Cannot refresh token: missing required data");
|
|
1076
1267
|
}
|
|
1077
|
-
const apiAuthBaseUrl = Config.
|
|
1268
|
+
const apiAuthBaseUrl = Config.getAuthServerUrl();
|
|
1078
1269
|
const tokenUrl = `${apiAuthBaseUrl}/oauth/refresh`;
|
|
1079
1270
|
const body = new URLSearchParams({
|
|
1080
1271
|
grant_type: DEFAULT_REFRESH_GRANT_TYPE,
|
|
@@ -1133,6 +1324,56 @@ async function refreshAccessToken() {
|
|
|
1133
1324
|
return refreshPromise;
|
|
1134
1325
|
}
|
|
1135
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
|
+
|
|
1136
1377
|
// src/application/security/redirectLoopGuard.ts
|
|
1137
1378
|
var DEFAULT_MAX_REDIRECTS = 5;
|
|
1138
1379
|
var WINDOW_MS = 60 * 1e3;
|
|
@@ -1157,18 +1398,166 @@ function checkAndIncrementRedirect(maxAttempts = DEFAULT_MAX_REDIRECTS) {
|
|
|
1157
1398
|
redirectCount++;
|
|
1158
1399
|
return true;
|
|
1159
1400
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
+
logger.info("[initiateAuthFlow] authServerUrl for OAuth", {
|
|
1442
|
+
authServerFromProps: authServerFromProps || null,
|
|
1443
|
+
authServerEffective,
|
|
1444
|
+
redirectUriEffective,
|
|
1445
|
+
tenantId: tenantIdEffective,
|
|
1446
|
+
tenantKey: config.tenantKey ?? null,
|
|
1447
|
+
clientIdEffective
|
|
1448
|
+
});
|
|
1449
|
+
const token = getAccessToken();
|
|
1450
|
+
if (token && !isAccessTokenExpired()) {
|
|
1451
|
+
logger.debug("Valid token already exists, skipping auth flow");
|
|
1452
|
+
clearFlowFlags();
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (isCallbackUrl()) {
|
|
1456
|
+
logger.debug("Callback URL detected - clearing flow flag to allow callback processing");
|
|
1457
|
+
clearFlowFlags();
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
if (isFlowInProgress()) {
|
|
1461
|
+
if (isFlowFlagStale(3e3)) {
|
|
1462
|
+
logger.info("Stale auth flow flag detected, clearing and proceeding");
|
|
1463
|
+
clearFlowFlags();
|
|
1464
|
+
} else {
|
|
1465
|
+
logger.debug("Auth flow already in progress (recent), skipping duplicate redirect");
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
setFlowFlags();
|
|
1470
|
+
if (!(Config.getAuthServerUrl() || config?.authServer) || !config.client?.clientId || !config.client?.redirectUri) {
|
|
1471
|
+
logger.error("[initiateAuthFlow] Missing required tenant config", {
|
|
1472
|
+
authServerEffective,
|
|
1473
|
+
clientId: config?.client?.clientId,
|
|
1474
|
+
redirectUriFromTenantApi: config?.client?.redirectUri,
|
|
1475
|
+
redirectUriEffective
|
|
1476
|
+
});
|
|
1477
|
+
throw new Error("Invalid tenant configuration for auth flow");
|
|
1478
|
+
}
|
|
1479
|
+
storeTenantConfig(config);
|
|
1480
|
+
logger.debug("[initiateAuthFlow] Stored tenant config, generating PKCE");
|
|
1481
|
+
removeStorage(getStorageKey("callback_processed"));
|
|
1482
|
+
removeStorage(getStorageKey("state_mismatch"));
|
|
1483
|
+
clearCallbackProcessing();
|
|
1484
|
+
const codeVerifier = generateCodeVerifier();
|
|
1485
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
1486
|
+
const state = generateState();
|
|
1487
|
+
removeStorage(STORAGE_KEYS.codeVerifier);
|
|
1488
|
+
removeStorage(STORAGE_KEYS.state);
|
|
1489
|
+
setStorage(STORAGE_KEYS.codeVerifier, codeVerifier);
|
|
1490
|
+
setStorage(STORAGE_KEYS.state, state);
|
|
1491
|
+
const authUrl = buildAuthorizationUrl(config, codeChallenge, state);
|
|
1492
|
+
logger.debug("[initiateAuthFlow] Built authorization URL", {
|
|
1493
|
+
authServerEffective,
|
|
1494
|
+
urlPrefix: authUrl?.slice(0, 80)
|
|
1495
|
+
});
|
|
1496
|
+
if (!authUrl || !(authUrl.startsWith("http://") || authUrl.startsWith("https://"))) {
|
|
1497
|
+
throw new Error("Invalid authorization URL generated");
|
|
1498
|
+
}
|
|
1499
|
+
if (!checkAndIncrementRedirect()) {
|
|
1500
|
+
logger.warn("Redirect loop protection: max redirect attempts exceeded");
|
|
1501
|
+
clearFlowFlags();
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
logger.info("Redirecting to authorization server");
|
|
1505
|
+
emitAuthEvent(AuthEventNames.LOGIN_STARTED, { tenantKey: config.tenantKey });
|
|
1506
|
+
const runtime = getAuthRuntime();
|
|
1507
|
+
if (runtime) {
|
|
1508
|
+
logger.debug("[initiateAuthFlow] Redirect via runtime.urlProvider.redirect");
|
|
1509
|
+
runtime.urlProvider.redirect(authUrl);
|
|
1510
|
+
} else if (typeof window !== "undefined") {
|
|
1511
|
+
logger.debug("[initiateAuthFlow] Redirect via window.location.replace");
|
|
1512
|
+
window.location.replace(authUrl);
|
|
1513
|
+
} else {
|
|
1514
|
+
logger.error("[initiateAuthFlow] No runtime and no window; cannot redirect", { authUrl });
|
|
1515
|
+
}
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
clearFlowFlags();
|
|
1518
|
+
logger.error("Failed to initiate auth flow", {
|
|
1519
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1520
|
+
});
|
|
1521
|
+
throw error;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function buildAuthorizationUrl(config, codeChallenge, state) {
|
|
1525
|
+
const authServerBase = (Config.getAuthServerUrl() || config.authServer || "").replace(/\/+$/, "");
|
|
1526
|
+
if (isLocalhost2()) {
|
|
1527
|
+
logger.debug("Building authorization URL", {
|
|
1528
|
+
authServer: authServerBase,
|
|
1529
|
+
clientId: config.client.clientId,
|
|
1530
|
+
redirectUri: config.client.redirectUri,
|
|
1531
|
+
scopes: config.client.scopes
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
const tenantIdToUse = config.tenantId;
|
|
1535
|
+
const clientIdToUse = config.client.clientId;
|
|
1536
|
+
const redirectUriToUse = Config.getRedirectUri();
|
|
1537
|
+
const params = new URLSearchParams({
|
|
1538
|
+
client_id: `public-${clientIdToUse}`,
|
|
1539
|
+
tenant_id: tenantIdToUse,
|
|
1540
|
+
redirect_uri: redirectUriToUse,
|
|
1541
|
+
response_type: DEFAULT_RESPONSE_TYPE,
|
|
1542
|
+
scope: config.client.scopes,
|
|
1543
|
+
state,
|
|
1544
|
+
code_challenge: codeChallenge,
|
|
1545
|
+
code_challenge_method: DEFAULT_CODE_CHALLENGE_METHOD
|
|
1546
|
+
});
|
|
1547
|
+
return `${authServerBase}/connect/authorize?${params.toString()}`;
|
|
1548
|
+
}
|
|
1549
|
+
function isProcessed() {
|
|
1550
|
+
return getStorage(getStorageKey("callback_processed")) === "true";
|
|
1551
|
+
}
|
|
1552
|
+
function isProcessing() {
|
|
1553
|
+
return isCallbackProcessing();
|
|
1554
|
+
}
|
|
1555
|
+
function markProcessing() {
|
|
1556
|
+
setCallbackProcessing();
|
|
1557
|
+
}
|
|
1558
|
+
function markProcessed() {
|
|
1559
|
+
setStorage(getStorageKey("callback_processed"), "true");
|
|
1560
|
+
clearCallbackProcessing();
|
|
1172
1561
|
}
|
|
1173
1562
|
function clearProcessing() {
|
|
1174
1563
|
clearCallbackProcessing();
|
|
@@ -1187,6 +1576,38 @@ function cleanUrlSoft() {
|
|
|
1187
1576
|
function clearAllAuthFlags() {
|
|
1188
1577
|
clearFlowFlags();
|
|
1189
1578
|
}
|
|
1579
|
+
async function tryRecoverOAuthConfigFromTenant() {
|
|
1580
|
+
const runtime = getAuthRuntime();
|
|
1581
|
+
const tenantKey = (resolveTenantKey() ?? getTenantKey() ?? "").trim();
|
|
1582
|
+
if (!runtime?.tenantResolver || !tenantKey) {
|
|
1583
|
+
logger.debug("[Callback] OAuth config recovery skipped \u2014 no tenantResolver or tenantKey", {
|
|
1584
|
+
hasResolver: !!runtime?.tenantResolver,
|
|
1585
|
+
tenantKey: tenantKey || "(empty)"
|
|
1586
|
+
});
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
try {
|
|
1590
|
+
const tc = await runtime.tenantResolver.resolve(tenantKey);
|
|
1591
|
+
if (!tc?.client?.clientId || !tc.tenantId) {
|
|
1592
|
+
logger.warn("[Callback] OAuth config recovery: tenant resolve missing client or tenantId", {
|
|
1593
|
+
tenantKey
|
|
1594
|
+
});
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
storeTenantConfig(tc);
|
|
1598
|
+
return {
|
|
1599
|
+
clientId: tc.client.clientId.trim() || null,
|
|
1600
|
+
redirectUri: tc.client.redirectUri?.trim() || getRedirectUri(),
|
|
1601
|
+
tenantId: tc.tenantId.trim() || null
|
|
1602
|
+
};
|
|
1603
|
+
} catch (e) {
|
|
1604
|
+
logger.warn("[Callback] OAuth config recovery: tenant re-resolve failed", {
|
|
1605
|
+
tenantKey,
|
|
1606
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1607
|
+
});
|
|
1608
|
+
return null;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1190
1611
|
async function handleCallback() {
|
|
1191
1612
|
const runtimeSearch = getAuthRuntime()?.urlProvider.getCurrentUrl().search;
|
|
1192
1613
|
const windowSearch = typeof window !== "undefined" ? window.location.search : "";
|
|
@@ -1269,9 +1690,22 @@ async function handleCallback() {
|
|
|
1269
1690
|
cleanUrlSoft();
|
|
1270
1691
|
return false;
|
|
1271
1692
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1693
|
+
let clientId = getClientId();
|
|
1694
|
+
let redirectUri = getRedirectUri();
|
|
1695
|
+
let tenantId = getTenantId();
|
|
1696
|
+
if (!clientId || !redirectUri || !tenantId) {
|
|
1697
|
+
logger.warn("[Callback] OAuth config incomplete \u2014 attempting tenant re-resolve", {
|
|
1698
|
+
hasClientId: !!clientId,
|
|
1699
|
+
hasRedirectUri: !!redirectUri,
|
|
1700
|
+
hasTenantId: !!tenantId
|
|
1701
|
+
});
|
|
1702
|
+
const recovered = await tryRecoverOAuthConfigFromTenant();
|
|
1703
|
+
if (recovered) {
|
|
1704
|
+
if (!redirectUri) redirectUri = recovered.redirectUri;
|
|
1705
|
+
if (!tenantId) tenantId = recovered.tenantId;
|
|
1706
|
+
clientId = getClientId() ?? normalizeClientIdForOAuthTokenBody(recovered.clientId) ?? null;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1275
1709
|
if (!clientId || !redirectUri || !tenantId) {
|
|
1276
1710
|
logger.error("[Callback] Early exit: missing OAuth config during callback", {
|
|
1277
1711
|
hasClientId: !!clientId,
|
|
@@ -1329,320 +1763,120 @@ async function handleCallback() {
|
|
|
1329
1763
|
if (status === 400 && errorCode === "invalid_grant" && (String(errorDescription).includes("already used") || String(errorDescription).includes("Code not found") || String(errorDescription).includes("expired, already used"))) {
|
|
1330
1764
|
const existingToken2 = getAccessToken();
|
|
1331
1765
|
const tokenValid2 = existingToken2 && !isAccessTokenExpired();
|
|
1332
|
-
if (tokenValid2) {
|
|
1333
|
-
removeStorage(STORAGE_KEYS.codeVerifier);
|
|
1334
|
-
removeStorage(STORAGE_KEYS.state);
|
|
1335
|
-
clearAllAuthFlags();
|
|
1336
|
-
markProcessed();
|
|
1337
|
-
cleanUrlSoft();
|
|
1338
|
-
logger.info("OAuth callback processed successfully (code already used, but token exists)");
|
|
1339
|
-
resetRedirectLoopGuard();
|
|
1340
|
-
emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
|
|
1341
|
-
return true;
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
emitAuthEvent(AuthEventNames.CALLBACK_FAILED, {
|
|
1345
|
-
error: err.message,
|
|
1346
|
-
message: responseData?.error_description ?? err.message
|
|
1347
|
-
});
|
|
1348
|
-
logger.error("OAuth callback failed (Axios error)", {
|
|
1349
|
-
status,
|
|
1350
|
-
statusText: err.response?.statusText,
|
|
1351
|
-
responseData: err.response?.data,
|
|
1352
|
-
message: err.message,
|
|
1353
|
-
url: err.config?.url
|
|
1354
|
-
});
|
|
1355
|
-
} else {
|
|
1356
|
-
logger.error("OAuth callback failed", {
|
|
1357
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1358
|
-
});
|
|
1359
|
-
}
|
|
1360
|
-
clearAllAuthFlags();
|
|
1361
|
-
const existingToken = getAccessToken();
|
|
1362
|
-
const tokenValid = existingToken && !isAccessTokenExpired();
|
|
1363
|
-
if (tokenValid) {
|
|
1364
|
-
markProcessed();
|
|
1365
|
-
cleanUrlSoft();
|
|
1366
|
-
resetRedirectLoopGuard();
|
|
1367
|
-
emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
|
|
1368
|
-
return true;
|
|
1369
|
-
}
|
|
1370
|
-
emitAuthEvent(AuthEventNames.CALLBACK_FAILED, {
|
|
1371
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1372
|
-
});
|
|
1373
|
-
clearProcessing();
|
|
1374
|
-
if (isCallbackUrl()) cleanUrlSoft();
|
|
1375
|
-
return false;
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
// src/application/flows/CallbackFlow.ts
|
|
1380
|
-
var CallbackFlow = class {
|
|
1381
|
-
async execute(context) {
|
|
1382
|
-
const { runtime } = context;
|
|
1383
|
-
const { urlProvider, logger: log } = runtime;
|
|
1384
|
-
const getUrl = () => urlProvider.getCurrentUrl();
|
|
1385
|
-
clearFlowFlags();
|
|
1386
|
-
const handled = await handleCallback();
|
|
1387
|
-
if (handled) {
|
|
1388
|
-
if (isCallbackUrlFromUrl(getUrl())) {
|
|
1389
|
-
if (isCallbackProcessing()) return { completed: true };
|
|
1390
|
-
const u = getUrl();
|
|
1391
|
-
urlProvider.redirect(`${u.origin}${u.pathname}`);
|
|
1392
|
-
return { completed: true };
|
|
1393
|
-
}
|
|
1394
|
-
return { completed: true };
|
|
1395
|
-
}
|
|
1396
|
-
if (isCallbackUrlFromUrl(getUrl())) {
|
|
1397
|
-
const u = getUrl();
|
|
1398
|
-
urlProvider.redirect(`${u.origin}${u.pathname}`);
|
|
1399
|
-
}
|
|
1400
|
-
return { completed: true };
|
|
1401
|
-
}
|
|
1402
|
-
};
|
|
1403
|
-
|
|
1404
|
-
// src/application/flows/AuthenticatedFlow.ts
|
|
1405
|
-
var AuthenticatedFlow = class {
|
|
1406
|
-
async execute(_context) {
|
|
1407
|
-
return { completed: true };
|
|
1408
|
-
}
|
|
1409
|
-
};
|
|
1410
|
-
|
|
1411
|
-
// src/application/engine/defaultEngine.ts
|
|
1412
|
-
var defaultEngine = null;
|
|
1413
|
-
function getDefaultAuthEngine() {
|
|
1414
|
-
return defaultEngine;
|
|
1415
|
-
}
|
|
1416
|
-
function setDefaultAuthEngine(engine) {
|
|
1417
|
-
defaultEngine = engine;
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
// src/application/token.ts
|
|
1421
|
-
var lastNoRefreshLog = 0;
|
|
1422
|
-
var NO_REFRESH_LOG_INTERVAL_MS = 5e3;
|
|
1423
|
-
async function ensureValidAccessToken() {
|
|
1424
|
-
const engine = getDefaultAuthEngine();
|
|
1425
|
-
if (engine) {
|
|
1426
|
-
try {
|
|
1427
|
-
return await engine.getAccessToken();
|
|
1428
|
-
} catch {
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
const token = getAccessToken();
|
|
1432
|
-
if (token && !isAccessTokenExpired()) return token;
|
|
1433
|
-
const refreshToken = getRefreshToken();
|
|
1434
|
-
if (!refreshToken) {
|
|
1435
|
-
const now = Date.now();
|
|
1436
|
-
if (now - lastNoRefreshLog >= NO_REFRESH_LOG_INTERVAL_MS) {
|
|
1437
|
-
lastNoRefreshLog = now;
|
|
1438
|
-
logger.debug("No refresh token - cannot refresh; login required");
|
|
1439
|
-
}
|
|
1440
|
-
throw new Error("No refresh token; login required");
|
|
1441
|
-
}
|
|
1442
|
-
return await refreshAccessToken();
|
|
1443
|
-
}
|
|
1444
|
-
function getAccessToken2() {
|
|
1445
|
-
return getAccessToken();
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
// src/strategies/oauth2-pkce/pkce.ts
|
|
1449
|
-
function generateCodeVerifier(length = 128) {
|
|
1450
|
-
const array = new Uint8Array(length);
|
|
1451
|
-
crypto.getRandomValues(array);
|
|
1452
|
-
const base64 = btoa(String.fromCharCode(...array));
|
|
1453
|
-
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1454
|
-
}
|
|
1455
|
-
async function generateCodeChallenge(verifier) {
|
|
1456
|
-
try {
|
|
1457
|
-
const encoder = new TextEncoder();
|
|
1458
|
-
const data = encoder.encode(verifier);
|
|
1459
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
1460
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1461
|
-
const base64 = btoa(String.fromCharCode(...hashArray));
|
|
1462
|
-
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1463
|
-
} catch {
|
|
1464
|
-
throw new Error("Failed to generate code challenge");
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
function generateState() {
|
|
1468
|
-
const array = new Uint8Array(32);
|
|
1469
|
-
crypto.getRandomValues(array);
|
|
1470
|
-
const base64 = btoa(String.fromCharCode(...array));
|
|
1471
|
-
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
// src/utils/validation.ts
|
|
1475
|
-
function isValidGuid(guid) {
|
|
1476
|
-
if (!guid || typeof guid !== "string") {
|
|
1477
|
-
return false;
|
|
1478
|
-
}
|
|
1479
|
-
const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1480
|
-
return guidRegex.test(guid.trim());
|
|
1481
|
-
}
|
|
1482
|
-
function validateTenantId(tenantId) {
|
|
1483
|
-
if (!tenantId || tenantId.trim() === "") {
|
|
1484
|
-
return {
|
|
1485
|
-
isValid: false,
|
|
1486
|
-
error: "Tenant ID is required"
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
if (!isValidGuid(tenantId)) {
|
|
1490
|
-
return {
|
|
1491
|
-
isValid: false,
|
|
1492
|
-
error: "Tenant ID must be a valid GUID format (00000000-0000-0000-0000-000000000001)"
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
return { isValid: true };
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
// src/strategies/oauth2-pkce/oauth.ts
|
|
1499
|
-
function storeTenantConfig(config) {
|
|
1500
|
-
const validation = validateTenantId(config.tenantId);
|
|
1501
|
-
if (!validation.isValid) {
|
|
1502
|
-
logger.warn("Tenant ID validation failed", { tenantId: config.tenantId });
|
|
1503
|
-
}
|
|
1504
|
-
const authServerToStore = Config.getAuthServerUrl() || config.authServer;
|
|
1505
|
-
const redirectUriToStore = Config.getRedirectUri();
|
|
1506
|
-
const hostConfigKey = getStorageKey("host_config");
|
|
1507
|
-
let existing = {};
|
|
1508
|
-
try {
|
|
1509
|
-
const raw = getStorage(hostConfigKey);
|
|
1510
|
-
if (raw) existing = JSON.parse(raw);
|
|
1511
|
-
} catch {
|
|
1512
|
-
existing = {};
|
|
1513
|
-
}
|
|
1514
|
-
setStorage(
|
|
1515
|
-
hostConfigKey,
|
|
1516
|
-
JSON.stringify({
|
|
1517
|
-
...existing,
|
|
1518
|
-
authServerUrl: authServerToStore,
|
|
1519
|
-
clientId: config.client.clientId,
|
|
1520
|
-
redirectUri: redirectUriToStore,
|
|
1521
|
-
tenantId: config.tenantId,
|
|
1522
|
-
tenantKey: config.tenantKey ?? existing.tenantKey ?? "",
|
|
1523
|
-
tenantCode: config.tenantKey ?? existing.tenantCode ?? ""
|
|
1524
|
-
})
|
|
1525
|
-
);
|
|
1526
|
-
if (config.tenantKey) {
|
|
1527
|
-
setStorage(STORAGE_KEYS.tenantKey, config.tenantKey);
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
async function initiateAuthFlow(config) {
|
|
1531
|
-
try {
|
|
1532
|
-
logger.info("Initiating OAuth authorization flow");
|
|
1533
|
-
const authServerFromProps = Config.getAuthServerUrl();
|
|
1534
|
-
const authServerEffective = (authServerFromProps || config?.authServer || "").replace(/\/+$/, "");
|
|
1535
|
-
const redirectUriEffective = Config.getRedirectUri();
|
|
1536
|
-
const tenantIdEffective = config?.tenantId || "";
|
|
1537
|
-
const clientIdEffective = config?.client?.clientId || "";
|
|
1538
|
-
logger.debug("[initiateAuthFlow] Resolved inputs", {
|
|
1539
|
-
authServerFromProps: authServerFromProps ? "(provided)" : "(empty)",
|
|
1540
|
-
authServerEffective,
|
|
1541
|
-
redirectUriEffective,
|
|
1542
|
-
tenantIdEffective,
|
|
1543
|
-
clientIdEffective,
|
|
1544
|
-
scopes: config?.client?.scopes,
|
|
1545
|
-
hasRuntime: !!getAuthRuntime()
|
|
1546
|
-
});
|
|
1547
|
-
const token = getAccessToken();
|
|
1548
|
-
if (token && !isAccessTokenExpired()) {
|
|
1549
|
-
logger.debug("Valid token already exists, skipping auth flow");
|
|
1550
|
-
clearFlowFlags();
|
|
1551
|
-
return;
|
|
1552
|
-
}
|
|
1553
|
-
if (isCallbackUrl()) {
|
|
1554
|
-
logger.debug("Callback URL detected - clearing flow flag to allow callback processing");
|
|
1555
|
-
clearFlowFlags();
|
|
1556
|
-
return;
|
|
1557
|
-
}
|
|
1558
|
-
if (isFlowInProgress()) {
|
|
1559
|
-
if (isFlowFlagStale(3e3)) {
|
|
1560
|
-
logger.info("Stale auth flow flag detected, clearing and proceeding");
|
|
1561
|
-
clearFlowFlags();
|
|
1562
|
-
} else {
|
|
1563
|
-
logger.debug("Auth flow already in progress (recent), skipping duplicate redirect");
|
|
1564
|
-
return;
|
|
1766
|
+
if (tokenValid2) {
|
|
1767
|
+
removeStorage(STORAGE_KEYS.codeVerifier);
|
|
1768
|
+
removeStorage(STORAGE_KEYS.state);
|
|
1769
|
+
clearAllAuthFlags();
|
|
1770
|
+
markProcessed();
|
|
1771
|
+
cleanUrlSoft();
|
|
1772
|
+
logger.info("OAuth callback processed successfully (code already used, but token exists)");
|
|
1773
|
+
resetRedirectLoopGuard();
|
|
1774
|
+
emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
|
|
1775
|
+
return true;
|
|
1776
|
+
}
|
|
1565
1777
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1778
|
+
emitAuthEvent(AuthEventNames.CALLBACK_FAILED, {
|
|
1779
|
+
error: err.message,
|
|
1780
|
+
message: responseData?.error_description ?? err.message
|
|
1781
|
+
});
|
|
1782
|
+
logger.error("OAuth callback failed (Axios error)", {
|
|
1783
|
+
status,
|
|
1784
|
+
statusText: err.response?.statusText,
|
|
1785
|
+
responseData: err.response?.data,
|
|
1786
|
+
message: err.message,
|
|
1787
|
+
url: err.config?.url
|
|
1788
|
+
});
|
|
1789
|
+
} else {
|
|
1790
|
+
logger.error("OAuth callback failed", {
|
|
1791
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1574
1792
|
});
|
|
1575
|
-
throw new Error("Invalid tenant configuration for auth flow");
|
|
1576
1793
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
removeStorage(STORAGE_KEYS.state);
|
|
1587
|
-
setStorage(STORAGE_KEYS.codeVerifier, codeVerifier);
|
|
1588
|
-
setStorage(STORAGE_KEYS.state, state);
|
|
1589
|
-
const authUrl = buildAuthorizationUrl(config, codeChallenge, state);
|
|
1590
|
-
logger.debug("[initiateAuthFlow] Built authorization URL", {
|
|
1591
|
-
authServerEffective,
|
|
1592
|
-
urlPrefix: authUrl?.slice(0, 80)
|
|
1593
|
-
});
|
|
1594
|
-
if (!authUrl || !(authUrl.startsWith("http://") || authUrl.startsWith("https://"))) {
|
|
1595
|
-
throw new Error("Invalid authorization URL generated");
|
|
1794
|
+
clearAllAuthFlags();
|
|
1795
|
+
const existingToken = getAccessToken();
|
|
1796
|
+
const tokenValid = existingToken && !isAccessTokenExpired();
|
|
1797
|
+
if (tokenValid) {
|
|
1798
|
+
markProcessed();
|
|
1799
|
+
cleanUrlSoft();
|
|
1800
|
+
resetRedirectLoopGuard();
|
|
1801
|
+
emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
|
|
1802
|
+
return true;
|
|
1596
1803
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1804
|
+
emitAuthEvent(AuthEventNames.CALLBACK_FAILED, {
|
|
1805
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1806
|
+
});
|
|
1807
|
+
clearProcessing();
|
|
1808
|
+
if (isCallbackUrl()) cleanUrlSoft();
|
|
1809
|
+
return false;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/application/flows/CallbackFlow.ts
|
|
1814
|
+
var CallbackFlow = class {
|
|
1815
|
+
async execute(context) {
|
|
1816
|
+
const { runtime } = context;
|
|
1817
|
+
const { urlProvider, logger: log } = runtime;
|
|
1818
|
+
const getUrl = () => urlProvider.getCurrentUrl();
|
|
1819
|
+
clearFlowFlags();
|
|
1820
|
+
const handled = await handleCallback();
|
|
1821
|
+
if (handled) {
|
|
1822
|
+
if (isCallbackUrlFromUrl(getUrl())) {
|
|
1823
|
+
if (isCallbackProcessing()) return { completed: true };
|
|
1824
|
+
const u = getUrl();
|
|
1825
|
+
urlProvider.redirect(`${u.origin}${u.pathname}`);
|
|
1826
|
+
return { completed: true };
|
|
1827
|
+
}
|
|
1828
|
+
return { completed: true };
|
|
1601
1829
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
if (runtime) {
|
|
1606
|
-
logger.debug("[initiateAuthFlow] Redirect via runtime.urlProvider.redirect");
|
|
1607
|
-
runtime.urlProvider.redirect(authUrl);
|
|
1608
|
-
} else if (typeof window !== "undefined") {
|
|
1609
|
-
logger.debug("[initiateAuthFlow] Redirect via window.location.replace");
|
|
1610
|
-
window.location.replace(authUrl);
|
|
1611
|
-
} else {
|
|
1612
|
-
logger.error("[initiateAuthFlow] No runtime and no window; cannot redirect", { authUrl });
|
|
1830
|
+
if (isCallbackUrlFromUrl(getUrl())) {
|
|
1831
|
+
const u = getUrl();
|
|
1832
|
+
urlProvider.redirect(`${u.origin}${u.pathname}`);
|
|
1613
1833
|
}
|
|
1614
|
-
|
|
1615
|
-
clearFlowFlags();
|
|
1616
|
-
logger.error("Failed to initiate auth flow", {
|
|
1617
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1618
|
-
});
|
|
1619
|
-
throw error;
|
|
1834
|
+
return { completed: true };
|
|
1620
1835
|
}
|
|
1836
|
+
};
|
|
1837
|
+
|
|
1838
|
+
// src/application/flows/AuthenticatedFlow.ts
|
|
1839
|
+
var AuthenticatedFlow = class {
|
|
1840
|
+
async execute(_context) {
|
|
1841
|
+
return { completed: true };
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
|
|
1845
|
+
// src/application/engine/defaultEngine.ts
|
|
1846
|
+
var defaultEngine = null;
|
|
1847
|
+
function getDefaultAuthEngine() {
|
|
1848
|
+
return defaultEngine;
|
|
1621
1849
|
}
|
|
1622
|
-
function
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1850
|
+
function setDefaultAuthEngine(engine) {
|
|
1851
|
+
defaultEngine = engine;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/application/token.ts
|
|
1855
|
+
var lastNoRefreshLog = 0;
|
|
1856
|
+
var NO_REFRESH_LOG_INTERVAL_MS = 5e3;
|
|
1857
|
+
async function ensureValidAccessToken() {
|
|
1858
|
+
const engine = getDefaultAuthEngine();
|
|
1859
|
+
if (engine) {
|
|
1860
|
+
try {
|
|
1861
|
+
return await engine.getAccessToken();
|
|
1862
|
+
} catch {
|
|
1863
|
+
}
|
|
1631
1864
|
}
|
|
1632
|
-
const
|
|
1633
|
-
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1865
|
+
const token = getAccessToken();
|
|
1866
|
+
if (token && !isAccessTokenExpired()) return token;
|
|
1867
|
+
const refreshToken = getRefreshToken();
|
|
1868
|
+
if (!refreshToken) {
|
|
1869
|
+
const now = Date.now();
|
|
1870
|
+
if (now - lastNoRefreshLog >= NO_REFRESH_LOG_INTERVAL_MS) {
|
|
1871
|
+
lastNoRefreshLog = now;
|
|
1872
|
+
logger.debug("No refresh token - cannot refresh; login required");
|
|
1873
|
+
}
|
|
1874
|
+
throw new Error("No refresh token; login required");
|
|
1875
|
+
}
|
|
1876
|
+
return await refreshAccessToken();
|
|
1877
|
+
}
|
|
1878
|
+
function getAccessToken2() {
|
|
1879
|
+
return getAccessToken();
|
|
1646
1880
|
}
|
|
1647
1881
|
|
|
1648
1882
|
// src/application/flows/RefreshFlow.ts
|
|
@@ -1723,13 +1957,18 @@ var TenantSwitchFlow = class {
|
|
|
1723
1957
|
);
|
|
1724
1958
|
}
|
|
1725
1959
|
try {
|
|
1726
|
-
const cfg =
|
|
1727
|
-
|
|
1728
|
-
|
|
1960
|
+
const cfg = getAuthConfig2();
|
|
1961
|
+
writeHostConfigCache({
|
|
1962
|
+
apiBaseUrl: cfg.apiBaseUrl,
|
|
1963
|
+
authServerUrl: cfg.authServerUrl,
|
|
1964
|
+
callbackUrl: cfg.callbackUrl,
|
|
1965
|
+
appId: cfg.appId,
|
|
1966
|
+
clientCode: tenantConfig.client?.clientCode ?? cfg.clientCode ?? "",
|
|
1967
|
+
clientId: cfg.clientId || tenantConfig.client?.clientId,
|
|
1968
|
+
tenantId: tenantConfig.tenantId ?? cfg.tenantId,
|
|
1729
1969
|
tenantKey,
|
|
1730
|
-
tenantCode: tenantKey
|
|
1731
|
-
|
|
1732
|
-
}));
|
|
1970
|
+
tenantCode: tenantKey
|
|
1971
|
+
});
|
|
1733
1972
|
} catch {
|
|
1734
1973
|
}
|
|
1735
1974
|
setTenantKey(tenantKey);
|
|
@@ -1893,6 +2132,22 @@ async function bootstrap() {
|
|
|
1893
2132
|
return bootstrapImpl();
|
|
1894
2133
|
}
|
|
1895
2134
|
|
|
2135
|
+
// src/infrastructure/watchers/watcher-guard.ts
|
|
2136
|
+
var suspended = false;
|
|
2137
|
+
var generation = 0;
|
|
2138
|
+
function suspendWatchers() {
|
|
2139
|
+
suspended = true;
|
|
2140
|
+
generation += 1;
|
|
2141
|
+
return generation;
|
|
2142
|
+
}
|
|
2143
|
+
function resumeWatchers(expectedGen) {
|
|
2144
|
+
if (expectedGen !== void 0 && expectedGen !== generation) return;
|
|
2145
|
+
suspended = false;
|
|
2146
|
+
}
|
|
2147
|
+
function areWatchersSuspended() {
|
|
2148
|
+
return suspended;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
1896
2151
|
// src/strategies/cookie/cookie-auth.ts
|
|
1897
2152
|
var COOKIE_NAMES = {
|
|
1898
2153
|
accessToken: "tenneo_auth_access_token",
|
|
@@ -2019,6 +2274,10 @@ function checkCookiesDeleted() {
|
|
|
2019
2274
|
}
|
|
2020
2275
|
}
|
|
2021
2276
|
function handleCookieDeletion() {
|
|
2277
|
+
if (areWatchersSuspended()) {
|
|
2278
|
+
logger.debug("Cookie deletion handler skipped \u2014 watchers suspended (logout/teardown)");
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2022
2281
|
try {
|
|
2023
2282
|
logger.info("Cookies deleted detected - clearing session and redirecting to login");
|
|
2024
2283
|
clearAuthStorage();
|
|
@@ -2056,6 +2315,7 @@ function startCookieWatcher() {
|
|
|
2056
2315
|
allCookies: initialCookies
|
|
2057
2316
|
});
|
|
2058
2317
|
watchInterval = setInterval(async () => {
|
|
2318
|
+
if (areWatchersSuspended()) return;
|
|
2059
2319
|
const cookiesDeleted = checkCookiesDeleted();
|
|
2060
2320
|
if (cookiesDeleted) {
|
|
2061
2321
|
logger.info("Cookies deleted detected by cookie watcher");
|
|
@@ -2119,9 +2379,11 @@ function checkStorageCleared() {
|
|
|
2119
2379
|
}
|
|
2120
2380
|
}
|
|
2121
2381
|
function handleStorageEvent(event) {
|
|
2382
|
+
if (areWatchersSuspended()) return;
|
|
2122
2383
|
if (typeof window !== "undefined" && event.storageArea === window.sessionStorage) {
|
|
2123
2384
|
const wasCleared = checkStorageCleared();
|
|
2124
2385
|
if (wasCleared) {
|
|
2386
|
+
if (areWatchersSuspended()) return;
|
|
2125
2387
|
logger.info("Detected manual sessionStorage clear! Redirecting to login...");
|
|
2126
2388
|
const cb = getSessionClearedCallback();
|
|
2127
2389
|
if (cb) {
|
|
@@ -2141,8 +2403,10 @@ function startStorageWatcher() {
|
|
|
2141
2403
|
storageEventListener = handleStorageEvent;
|
|
2142
2404
|
window.addEventListener("storage", storageEventListener);
|
|
2143
2405
|
const pollInterval = setInterval(() => {
|
|
2406
|
+
if (areWatchersSuspended()) return;
|
|
2144
2407
|
const wasCleared = checkStorageCleared();
|
|
2145
2408
|
if (wasCleared) {
|
|
2409
|
+
if (areWatchersSuspended()) return;
|
|
2146
2410
|
logger.info("Detected manual sessionStorage clear (polling)! Redirecting to login...");
|
|
2147
2411
|
clearInterval(pollInterval);
|
|
2148
2412
|
const cb = getSessionClearedCallback();
|
|
@@ -2213,27 +2477,6 @@ function buildUrl(baseUrl, path) {
|
|
|
2213
2477
|
url.pathname = url.pathname.endsWith("/") ? `${url.pathname}${normalizedPath.slice(1)}` : `${url.pathname}${normalizedPath}`;
|
|
2214
2478
|
return url.toString();
|
|
2215
2479
|
}
|
|
2216
|
-
function snapshotHostConfig() {
|
|
2217
|
-
try {
|
|
2218
|
-
const raw = getStorage(getStorageKey("host_config"));
|
|
2219
|
-
if (!raw) return null;
|
|
2220
|
-
const parsed = JSON.parse(raw);
|
|
2221
|
-
return parsed && typeof parsed === "object" ? parsed : null;
|
|
2222
|
-
} catch {
|
|
2223
|
-
return null;
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
function restoreHostConfig(config) {
|
|
2227
|
-
if (!config) return;
|
|
2228
|
-
try {
|
|
2229
|
-
setStorage(getStorageKey("host_config"), JSON.stringify(config));
|
|
2230
|
-
logger.debug("Restored host_config after logout cleanup");
|
|
2231
|
-
} catch (error) {
|
|
2232
|
-
logger.warn("Failed to restore host_config after logout cleanup", {
|
|
2233
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2234
|
-
});
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
2480
|
function getCsrfToken() {
|
|
2238
2481
|
try {
|
|
2239
2482
|
if (typeof document === "undefined") return null;
|
|
@@ -2248,62 +2491,68 @@ function getCsrfToken() {
|
|
|
2248
2491
|
}
|
|
2249
2492
|
async function callServerLogout(logoutUrl) {
|
|
2250
2493
|
const csrfToken = getCsrfToken();
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
if (csrfToken) {
|
|
2261
|
-
headers["X-CSRF-Token"] = csrfToken;
|
|
2262
|
-
}
|
|
2494
|
+
const headers = {
|
|
2495
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
2496
|
+
Accept: "application/json, */*",
|
|
2497
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2498
|
+
};
|
|
2499
|
+
if (csrfToken) {
|
|
2500
|
+
headers["X-CSRF-Token"] = csrfToken;
|
|
2501
|
+
}
|
|
2502
|
+
try {
|
|
2263
2503
|
const response = await fetch(logoutUrl, {
|
|
2264
|
-
method,
|
|
2504
|
+
method: "POST",
|
|
2265
2505
|
mode: "cors",
|
|
2266
2506
|
headers,
|
|
2267
|
-
credentials: "include"
|
|
2507
|
+
credentials: "include",
|
|
2508
|
+
body: ""
|
|
2268
2509
|
});
|
|
2269
2510
|
if (!response.ok) {
|
|
2270
|
-
logger.warn("Server logout HTTP error", {
|
|
2271
|
-
method,
|
|
2511
|
+
logger.warn("[logout] Server logout HTTP error", {
|
|
2272
2512
|
status: response.status,
|
|
2273
2513
|
statusText: response.statusText
|
|
2274
2514
|
});
|
|
2275
2515
|
return { success: false };
|
|
2276
2516
|
}
|
|
2277
|
-
const
|
|
2278
|
-
|
|
2517
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2518
|
+
if (!contentType.includes("application/json")) {
|
|
2519
|
+
logger.info("[logout] Server logout: OK without JSON body", {
|
|
2520
|
+
status: response.status
|
|
2521
|
+
});
|
|
2522
|
+
return { success: true, response: { success: true } };
|
|
2523
|
+
}
|
|
2524
|
+
const text = await response.text();
|
|
2525
|
+
let data = {};
|
|
2526
|
+
if (text?.trim()) {
|
|
2527
|
+
try {
|
|
2528
|
+
data = JSON.parse(text);
|
|
2529
|
+
} catch {
|
|
2530
|
+
logger.warn("[logout] Server returned non-JSON body with JSON content-type");
|
|
2531
|
+
return { success: false };
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
logger.info("[logout] Server logout response", { response: data });
|
|
2279
2535
|
if (data && (data.success === true || data.logout === true)) {
|
|
2280
2536
|
return { success: true, response: data };
|
|
2281
2537
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
});
|
|
2538
|
+
if (!text || text.trim() === "") {
|
|
2539
|
+
return { success: true, response: { success: true } };
|
|
2540
|
+
}
|
|
2541
|
+
logger.warn("[logout] Server logout: unexpected JSON payload", { response: data });
|
|
2286
2542
|
return { success: false, response: data };
|
|
2287
|
-
}
|
|
2288
|
-
try {
|
|
2289
|
-
const postResult = await doRequest("POST");
|
|
2290
|
-
if (postResult.success) return postResult;
|
|
2291
|
-
logger.debug("POST logout did not succeed, trying GET");
|
|
2292
|
-
const getResult = await doRequest("GET");
|
|
2293
|
-
return getResult;
|
|
2294
2543
|
} catch (error) {
|
|
2295
|
-
logger.warn("Server logout failed
|
|
2544
|
+
logger.warn("[logout] Server logout request failed", {
|
|
2296
2545
|
error: error instanceof Error ? error.message : String(error)
|
|
2297
2546
|
});
|
|
2298
2547
|
return { success: false };
|
|
2299
2548
|
}
|
|
2300
2549
|
}
|
|
2301
2550
|
function performLocalLogoutCleanup() {
|
|
2302
|
-
logger.debug("
|
|
2551
|
+
logger.debug("[logout] Local cleanup: cookies + auth storage");
|
|
2303
2552
|
try {
|
|
2304
2553
|
getAuthRuntime()?.clearCookies?.clearCookies();
|
|
2305
2554
|
} catch (error) {
|
|
2306
|
-
logger.debug("Runtime cookie clearer failed (ignored)", {
|
|
2555
|
+
logger.debug("[logout] Runtime cookie clearer failed (ignored)", {
|
|
2307
2556
|
error: error instanceof Error ? error.message : String(error)
|
|
2308
2557
|
});
|
|
2309
2558
|
}
|
|
@@ -2311,11 +2560,11 @@ function performLocalLogoutCleanup() {
|
|
|
2311
2560
|
try {
|
|
2312
2561
|
clearAuthStorage();
|
|
2313
2562
|
} catch (error) {
|
|
2314
|
-
logger.warn("clearAuthStorage failed
|
|
2563
|
+
logger.warn("[logout] clearAuthStorage failed", {
|
|
2315
2564
|
error: error instanceof Error ? error.message : String(error)
|
|
2316
2565
|
});
|
|
2317
2566
|
}
|
|
2318
|
-
logger.info("Local
|
|
2567
|
+
logger.info("[logout] Local cleanup completed");
|
|
2319
2568
|
}
|
|
2320
2569
|
function acquireLogoutLock() {
|
|
2321
2570
|
const LOCK_KEY = getStorageKey("logout_lock");
|
|
@@ -2324,7 +2573,7 @@ function acquireLogoutLock() {
|
|
|
2324
2573
|
if (existingLock) {
|
|
2325
2574
|
const lockTime = Number(existingLock);
|
|
2326
2575
|
if (!Number.isNaN(lockTime) && now - lockTime < 5e3) {
|
|
2327
|
-
logger.debug("
|
|
2576
|
+
logger.debug("[logout] Lock active \u2014 duplicate tab logout skipped", {
|
|
2328
2577
|
now,
|
|
2329
2578
|
lockTime
|
|
2330
2579
|
});
|
|
@@ -2340,72 +2589,95 @@ function releaseLogoutLock() {
|
|
|
2340
2589
|
} catch {
|
|
2341
2590
|
}
|
|
2342
2591
|
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
tenantCode: tenantKey.trim()
|
|
2350
|
-
};
|
|
2351
|
-
setStorage(getStorageKey("host_config"), JSON.stringify(updated));
|
|
2352
|
-
}
|
|
2592
|
+
var logoutInFlight = null;
|
|
2593
|
+
async function runLogoutPipeline(tenantKey) {
|
|
2594
|
+
const gen = suspendWatchers();
|
|
2595
|
+
logger.info("[logout] Lifecycle: suspend watchers", { generation: gen });
|
|
2596
|
+
stopStorageWatcher();
|
|
2597
|
+
stopCookieWatcher();
|
|
2353
2598
|
if (!acquireLogoutLock()) {
|
|
2354
|
-
|
|
2599
|
+
resumeWatchers(gen);
|
|
2600
|
+
logger.debug("[logout] Lifecycle: resume (skipped \u2014 lock)");
|
|
2601
|
+
return { success: true, message: "Logout already in progress in another tab." };
|
|
2355
2602
|
}
|
|
2603
|
+
let serverLogoutSuccess = false;
|
|
2604
|
+
let serverResponse;
|
|
2356
2605
|
try {
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2606
|
+
if (tenantKey?.trim()) {
|
|
2607
|
+
applyRuntimeTenantSwitch(tenantKey.trim(), { syncStorageCache: true });
|
|
2608
|
+
logger.info("[logout] Runtime tenant applied for logout context", {
|
|
2609
|
+
tenantKey: tenantKey.trim()
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
const authConfig = getAuthConfig2();
|
|
2613
|
+
const apiAuthBaseUrl = authConfig.authServerUrl;
|
|
2363
2614
|
if (apiAuthBaseUrl) {
|
|
2364
2615
|
const logoutUrl = buildUrl(apiAuthBaseUrl, "account/logout");
|
|
2365
|
-
logger.info("Calling auth server logout", { logoutUrl });
|
|
2616
|
+
logger.info("[logout] Calling auth server logout (POST)", { logoutUrl });
|
|
2366
2617
|
const logoutResult = await callServerLogout(logoutUrl);
|
|
2367
2618
|
serverLogoutSuccess = logoutResult.success;
|
|
2368
2619
|
serverResponse = logoutResult.response;
|
|
2369
2620
|
if (!serverLogoutSuccess) {
|
|
2370
|
-
logger.warn("Server logout
|
|
2621
|
+
logger.warn("[logout] Server logout did not confirm success \u2014 clearing local session anyway", {
|
|
2371
2622
|
logoutUrl,
|
|
2372
2623
|
serverResponse
|
|
2373
2624
|
});
|
|
2374
2625
|
}
|
|
2375
2626
|
} else {
|
|
2376
|
-
logger.warn("
|
|
2627
|
+
logger.warn("[logout] authServerUrl missing \u2014 local logout only");
|
|
2377
2628
|
}
|
|
2629
|
+
} catch (error) {
|
|
2630
|
+
logger.error("[logout] Unexpected error before local cleanup", {
|
|
2631
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2632
|
+
});
|
|
2633
|
+
}
|
|
2634
|
+
try {
|
|
2378
2635
|
performLocalLogoutCleanup();
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
releaseLogoutLock();
|
|
2383
|
-
logger.info("Logout completed \u2013 flags set, storage cleared, fresh login required", {
|
|
2384
|
-
serverLogoutSuccess,
|
|
2385
|
-
serverResponse
|
|
2636
|
+
} catch (error) {
|
|
2637
|
+
logger.error("[logout] Local cleanup threw", {
|
|
2638
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2386
2639
|
});
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2640
|
+
}
|
|
2641
|
+
try {
|
|
2642
|
+
syncHostConfigCacheFromMemory();
|
|
2643
|
+
logger.debug("[logout] host_config cache refreshed from runtime (not restored from snapshot)");
|
|
2644
|
+
} catch (error) {
|
|
2645
|
+
logger.warn("[logout] Failed to sync host config cache", {
|
|
2646
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2391
2647
|
});
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2648
|
+
}
|
|
2649
|
+
try {
|
|
2650
|
+
resetCookieWatcher();
|
|
2651
|
+
startStorageWatcher();
|
|
2652
|
+
logger.info("[logout] Watchers restarted");
|
|
2397
2653
|
} catch (error) {
|
|
2398
|
-
logger.
|
|
2654
|
+
logger.warn("[logout] Watcher restart failed", {
|
|
2399
2655
|
error: error instanceof Error ? error.message : String(error)
|
|
2400
2656
|
});
|
|
2401
|
-
performLocalLogoutCleanup();
|
|
2402
|
-
restoreHostConfig(preservedHostConfig);
|
|
2403
|
-
releaseLogoutLock();
|
|
2404
|
-
return {
|
|
2405
|
-
success: true,
|
|
2406
|
-
message: "Logged out locally due to an error. Fresh login required."
|
|
2407
|
-
};
|
|
2408
2657
|
}
|
|
2658
|
+
releaseLogoutLock();
|
|
2659
|
+
resumeWatchers(gen);
|
|
2660
|
+
logger.info("[logout] Lifecycle: complete", { serverLogoutSuccess });
|
|
2661
|
+
emitAuthEvent(AuthEventNames.LOGOUT_COMPLETED, {
|
|
2662
|
+
serverLogoutSuccess,
|
|
2663
|
+
tenantKey: tenantKey?.trim() || getAuthConfig2().tenantCode || void 0,
|
|
2664
|
+
message: serverLogoutSuccess ? "Server logout success" : "Local logout (server may be pending)"
|
|
2665
|
+
});
|
|
2666
|
+
return {
|
|
2667
|
+
success: true,
|
|
2668
|
+
message: serverLogoutSuccess ? serverResponse?.message || "Logout successful. Fresh login required." : "Logged out locally. Fresh login required (server logout may have failed).",
|
|
2669
|
+
serverResponse
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
async function logout(tenantKey) {
|
|
2673
|
+
if (logoutInFlight) {
|
|
2674
|
+
logger.debug("[logout] Coalescing with in-flight logout");
|
|
2675
|
+
return logoutInFlight;
|
|
2676
|
+
}
|
|
2677
|
+
logoutInFlight = runLogoutPipeline(tenantKey).finally(() => {
|
|
2678
|
+
logoutInFlight = null;
|
|
2679
|
+
});
|
|
2680
|
+
return logoutInFlight;
|
|
2409
2681
|
}
|
|
2410
2682
|
var AuthContext = createContext(void 0);
|
|
2411
2683
|
function useAuthContext() {
|
|
@@ -2480,7 +2752,6 @@ async function resolveTenant(tenantKey) {
|
|
|
2480
2752
|
scopes: DEFAULT_SCOPES2
|
|
2481
2753
|
}
|
|
2482
2754
|
};
|
|
2483
|
-
console.log("config====", config);
|
|
2484
2755
|
logger.debug("Resolved tenant config", { tenantCode });
|
|
2485
2756
|
return config;
|
|
2486
2757
|
} catch (error) {
|
|
@@ -2741,33 +3012,29 @@ function AuthProvider({
|
|
|
2741
3012
|
logger.warn("[AuthProvider] callbackUrl missing from host props");
|
|
2742
3013
|
}
|
|
2743
3014
|
setConfig({
|
|
2744
|
-
...mergedTenantCode ? { tenantCode: mergedTenantCode } : {},
|
|
3015
|
+
...mergedTenantCode ? { tenantCode: mergedTenantCode, tenantKey: mergedTenantCode } : {},
|
|
2745
3016
|
...mergedApiBaseUrl ? { apiBaseUrl: mergedApiBaseUrl } : {},
|
|
2746
3017
|
...mergedAuthServerUrl ? { authServerUrl: mergedAuthServerUrl } : {},
|
|
2747
3018
|
...mergedCallbackUrl ? { callbackUrl: mergedCallbackUrl } : {}
|
|
2748
3019
|
});
|
|
3020
|
+
logConfigResolutionDebug("AuthProvider host props applied");
|
|
2749
3021
|
return () => {
|
|
2750
3022
|
clearConfigOverrides();
|
|
2751
3023
|
};
|
|
2752
3024
|
}, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
|
|
2753
3025
|
useEffect(() => {
|
|
2754
3026
|
try {
|
|
2755
|
-
|
|
2756
|
-
const raw = getStorage(hostConfigKey);
|
|
2757
|
-
const existing = raw ? JSON.parse(raw) : {};
|
|
2758
|
-
const payload = {
|
|
2759
|
-
...existing,
|
|
3027
|
+
writeHostConfigCache({
|
|
2760
3028
|
tenantCode: mergedTenantCode ?? "",
|
|
2761
3029
|
tenantKey: mergedTenantCode ?? "",
|
|
2762
3030
|
appId: appId ?? "",
|
|
2763
3031
|
apiBaseUrl: mergedApiBaseUrl ?? "",
|
|
2764
3032
|
authServerUrl: mergedAuthServerUrl ?? "",
|
|
2765
3033
|
callbackUrl: mergedCallbackUrl ?? ""
|
|
2766
|
-
};
|
|
2767
|
-
setStorage(hostConfigKey, JSON.stringify(payload));
|
|
3034
|
+
});
|
|
2768
3035
|
} catch {
|
|
2769
3036
|
}
|
|
2770
|
-
}, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
|
|
3037
|
+
}, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl, appId]);
|
|
2771
3038
|
useLayoutEffect(() => {
|
|
2772
3039
|
setStorageKeyPrefix(resolvedTenantKey ?? "");
|
|
2773
3040
|
const adapter = storageAdapter !== void 0 && storageAdapter !== null ? storageAdapter : createSecureSessionStorageAdapter(namespace);
|
|
@@ -2833,14 +3100,14 @@ function AuthProvider({
|
|
|
2833
3100
|
logger.debug("[AuthProvider] TenantKey unchanged", { tenantKey: resolvedTenantKey2 });
|
|
2834
3101
|
}
|
|
2835
3102
|
try {
|
|
2836
|
-
|
|
2837
|
-
const raw = getStorage(hostConfigKey);
|
|
2838
|
-
const existing = raw ? JSON.parse(raw) : {};
|
|
2839
|
-
setStorage(hostConfigKey, JSON.stringify({
|
|
2840
|
-
...existing,
|
|
3103
|
+
writeHostConfigCache({
|
|
2841
3104
|
tenantKey: resolvedTenantKey2,
|
|
2842
3105
|
tenantCode: resolvedTenantKey2
|
|
2843
|
-
})
|
|
3106
|
+
});
|
|
3107
|
+
logConfigResolutionDebug("AuthProvider tenant key synced to cache", {
|
|
3108
|
+
tenantCode: resolvedTenantKey2,
|
|
3109
|
+
tenantKey: resolvedTenantKey2
|
|
3110
|
+
});
|
|
2844
3111
|
} catch {
|
|
2845
3112
|
}
|
|
2846
3113
|
setStorage(getStorageKey("tenant_key"), resolvedTenantKey2);
|
|
@@ -3244,6 +3511,6 @@ function useAuthInit() {
|
|
|
3244
3511
|
);
|
|
3245
3512
|
}
|
|
3246
3513
|
|
|
3247
|
-
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, getAuthConfig, getAuthRuntime, getConfig, getStorageAdapter, getStorageNamespace, getAccessToken as getStoredAccessToken, isAccessTokenExpired, isCallbackUrl, logout, refreshAccessToken, resetCookieWatcher, resolveConfig, resolveTenant, sanitizeErrorMessage, setAuthRuntime, setConfig, setStorageContext, startCookieWatcher, stopCookieWatcher, subscribeAuthEvent, useAuth, useAuthContext, useAuthInit };
|
|
3514
|
+
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 };
|
|
3248
3515
|
//# sourceMappingURL=index.mjs.map
|
|
3249
3516
|
//# sourceMappingURL=index.mjs.map
|