tenneo-auth-gateway-plugin 1.0.7
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/README.md +422 -0
- package/dist/index.d.mts +626 -0
- package/dist/index.d.ts +626 -0
- package/dist/index.js +3585 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3520 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +53 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3520 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createContext, useContext, useMemo, useRef, useLayoutEffect, useEffect, useState, useCallback } from 'react';
|
|
3
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/infrastructure/runtime/context.ts
|
|
6
|
+
var currentRuntime = null;
|
|
7
|
+
function setAuthRuntime(runtime) {
|
|
8
|
+
currentRuntime = runtime;
|
|
9
|
+
}
|
|
10
|
+
function getAuthRuntime() {
|
|
11
|
+
return currentRuntime;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/domain/tenant.ts
|
|
15
|
+
function isLocalhost(hostname) {
|
|
16
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.endsWith(".local");
|
|
17
|
+
}
|
|
18
|
+
function isCallbackUrlFromUrl(url) {
|
|
19
|
+
const params = new URLSearchParams(url.search);
|
|
20
|
+
return params.has("code") && params.has("state");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/infrastructure/storage/prefix.ts
|
|
24
|
+
var DEFAULT_PREFIX = "tenneo_auth_";
|
|
25
|
+
var currentPrefix = DEFAULT_PREFIX;
|
|
26
|
+
function getStorageKeyPrefix() {
|
|
27
|
+
return currentPrefix;
|
|
28
|
+
}
|
|
29
|
+
function normalizeTenantCode(tenantCode) {
|
|
30
|
+
const trimmed = tenantCode.trim();
|
|
31
|
+
if (!trimmed) return "";
|
|
32
|
+
return trimmed.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
function setStorageKeyPrefix(tenantCode) {
|
|
35
|
+
const normalized = normalizeTenantCode(tenantCode);
|
|
36
|
+
currentPrefix = normalized ? `${normalized}_auth_` : DEFAULT_PREFIX;
|
|
37
|
+
}
|
|
38
|
+
function getStorageKey(suffix) {
|
|
39
|
+
return getStorageKeyPrefix() + suffix;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/infrastructure/storage/namespace.ts
|
|
43
|
+
function buildStorageKey(ns, suffix) {
|
|
44
|
+
const { tenantId, appId } = ns;
|
|
45
|
+
if (!tenantId && !appId) {
|
|
46
|
+
return `${getStorageKeyPrefix()}${suffix}`;
|
|
47
|
+
}
|
|
48
|
+
const tid = tenantId || "";
|
|
49
|
+
const aid = appId || "";
|
|
50
|
+
return `auth_${tid}_${aid}_${suffix}`;
|
|
51
|
+
}
|
|
52
|
+
var TOKEN_KEY_SUFFIXES = {
|
|
53
|
+
accessToken: "access_token",
|
|
54
|
+
refreshToken: "refresh_token",
|
|
55
|
+
expiresAt: "expires_at"
|
|
56
|
+
};
|
|
57
|
+
var CONFIG_KEY_SUFFIXES = {
|
|
58
|
+
authServerUrl: "auth_server_url",
|
|
59
|
+
clientId: "client_id",
|
|
60
|
+
redirectUri: "redirect_uri",
|
|
61
|
+
tenantId: "tenant_id",
|
|
62
|
+
tenantKey: "tenant_key",
|
|
63
|
+
codeVerifier: "code_verifier",
|
|
64
|
+
state: "state",
|
|
65
|
+
callbackProcessed: "callback_processed",
|
|
66
|
+
callbackProcessing: "callback_processing",
|
|
67
|
+
callbackAttempted: "callback_attempted",
|
|
68
|
+
flowInProgress: "flow_in_progress",
|
|
69
|
+
flowTimestamp: "flow_timestamp",
|
|
70
|
+
logoutProcessing: "logout_processing",
|
|
71
|
+
propsConfig: "props_config",
|
|
72
|
+
propsChanged: "props_changed",
|
|
73
|
+
tenantSwitched: "tenant_switched",
|
|
74
|
+
newTenantKey: "new_tenant_key",
|
|
75
|
+
stateMismatch: "state_mismatch",
|
|
76
|
+
bootstrapInProgress: "bootstrap_in_progress",
|
|
77
|
+
bootstrapTimestamp: "bootstrap_timestamp",
|
|
78
|
+
callbackHandled: "callback_handled"
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/infrastructure/storage/context.ts
|
|
82
|
+
var currentAdapter = null;
|
|
83
|
+
var currentNamespace = { tenantId: "", appId: "" };
|
|
84
|
+
function setStorageContext(adapter, namespace = {}) {
|
|
85
|
+
currentAdapter = adapter;
|
|
86
|
+
currentNamespace = {
|
|
87
|
+
tenantId: namespace.tenantId ?? "",
|
|
88
|
+
appId: namespace.appId ?? ""
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function getStorageAdapter() {
|
|
92
|
+
const runtime = getAuthRuntime();
|
|
93
|
+
if (runtime?.storageAdapter) return runtime.storageAdapter;
|
|
94
|
+
return currentAdapter;
|
|
95
|
+
}
|
|
96
|
+
function getStorageNamespace() {
|
|
97
|
+
const runtime = getAuthRuntime();
|
|
98
|
+
if (runtime?.storageNamespace) return { ...runtime.storageNamespace };
|
|
99
|
+
return { ...currentNamespace };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/infrastructure/storage/memory-adapter.ts
|
|
103
|
+
function createMemoryStorageAdapter(namespace) {
|
|
104
|
+
const store = /* @__PURE__ */ new Map();
|
|
105
|
+
const getTokensSync = () => {
|
|
106
|
+
const accessToken = store.get(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.accessToken)) ?? null;
|
|
107
|
+
const refreshToken = store.get(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.refreshToken)) ?? null;
|
|
108
|
+
const expiresAtStr = store.get(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.expiresAt)) ?? null;
|
|
109
|
+
if (!accessToken || !refreshToken || !expiresAtStr) return null;
|
|
110
|
+
const expiresAt = parseInt(expiresAtStr, 10);
|
|
111
|
+
if (isNaN(expiresAt)) return null;
|
|
112
|
+
return { accessToken, refreshToken, expiresAt };
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
async getTokens() {
|
|
116
|
+
return getTokensSync();
|
|
117
|
+
},
|
|
118
|
+
getTokensSync,
|
|
119
|
+
async setTokens(tokens) {
|
|
120
|
+
store.set(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.accessToken), tokens.accessToken);
|
|
121
|
+
store.set(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.refreshToken), tokens.refreshToken);
|
|
122
|
+
store.set(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.expiresAt), String(tokens.expiresAt));
|
|
123
|
+
},
|
|
124
|
+
setTokensSync(tokens) {
|
|
125
|
+
store.set(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.accessToken), tokens.accessToken);
|
|
126
|
+
store.set(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.refreshToken), tokens.refreshToken);
|
|
127
|
+
store.set(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.expiresAt), String(tokens.expiresAt));
|
|
128
|
+
},
|
|
129
|
+
async clearTokens() {
|
|
130
|
+
store.delete(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.accessToken));
|
|
131
|
+
store.delete(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.refreshToken));
|
|
132
|
+
store.delete(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.expiresAt));
|
|
133
|
+
},
|
|
134
|
+
clearTokensSync() {
|
|
135
|
+
store.delete(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.accessToken));
|
|
136
|
+
store.delete(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.refreshToken));
|
|
137
|
+
store.delete(buildStorageKey(namespace, TOKEN_KEY_SUFFIXES.expiresAt));
|
|
138
|
+
},
|
|
139
|
+
async getItem(key) {
|
|
140
|
+
return store.get(key) ?? null;
|
|
141
|
+
},
|
|
142
|
+
getItemSync(key) {
|
|
143
|
+
return store.get(key) ?? null;
|
|
144
|
+
},
|
|
145
|
+
async setItem(key, value) {
|
|
146
|
+
store.set(key, value);
|
|
147
|
+
},
|
|
148
|
+
setItemSync(key, value) {
|
|
149
|
+
store.set(key, value);
|
|
150
|
+
},
|
|
151
|
+
async removeItem(key) {
|
|
152
|
+
store.delete(key);
|
|
153
|
+
},
|
|
154
|
+
removeItemSync(key) {
|
|
155
|
+
store.delete(key);
|
|
156
|
+
},
|
|
157
|
+
async clear() {
|
|
158
|
+
store.clear();
|
|
159
|
+
},
|
|
160
|
+
clearSync() {
|
|
161
|
+
store.clear();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/infrastructure/storage/session-adapter.ts
|
|
167
|
+
function getSessionStorage() {
|
|
168
|
+
if (typeof window === "undefined") return null;
|
|
169
|
+
try {
|
|
170
|
+
return window.sessionStorage;
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function createSecureSessionStorageAdapter(namespace) {
|
|
176
|
+
return {
|
|
177
|
+
async getTokens() {
|
|
178
|
+
return getTokensSyncImpl(namespace);
|
|
179
|
+
},
|
|
180
|
+
getTokensSync() {
|
|
181
|
+
return getTokensSyncImpl(namespace);
|
|
182
|
+
},
|
|
183
|
+
async setTokens(tokens) {
|
|
184
|
+
setTokensSyncImpl(namespace, tokens);
|
|
185
|
+
},
|
|
186
|
+
setTokensSync(tokens) {
|
|
187
|
+
setTokensSyncImpl(namespace, tokens);
|
|
188
|
+
},
|
|
189
|
+
async clearTokens() {
|
|
190
|
+
clearTokensSyncImpl(namespace);
|
|
191
|
+
},
|
|
192
|
+
clearTokensSync() {
|
|
193
|
+
clearTokensSyncImpl(namespace);
|
|
194
|
+
},
|
|
195
|
+
async getItem(key) {
|
|
196
|
+
return getItemSyncImpl(key);
|
|
197
|
+
},
|
|
198
|
+
getItemSync(key) {
|
|
199
|
+
return getItemSyncImpl(key);
|
|
200
|
+
},
|
|
201
|
+
async setItem(key, value) {
|
|
202
|
+
setItemSyncImpl(key, value);
|
|
203
|
+
},
|
|
204
|
+
setItemSync(key, value) {
|
|
205
|
+
setItemSyncImpl(key, value);
|
|
206
|
+
},
|
|
207
|
+
async removeItem(key) {
|
|
208
|
+
removeItemSyncImpl(key);
|
|
209
|
+
},
|
|
210
|
+
removeItemSync(key) {
|
|
211
|
+
removeItemSyncImpl(key);
|
|
212
|
+
},
|
|
213
|
+
async clear() {
|
|
214
|
+
clearSyncImpl(namespace);
|
|
215
|
+
},
|
|
216
|
+
clearSync() {
|
|
217
|
+
clearSyncImpl(namespace);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function getTokensSyncImpl(ns) {
|
|
222
|
+
const storage = getSessionStorage();
|
|
223
|
+
if (!storage) return null;
|
|
224
|
+
try {
|
|
225
|
+
const accessToken = storage.getItem(buildStorageKey(ns, TOKEN_KEY_SUFFIXES.accessToken));
|
|
226
|
+
const refreshToken = storage.getItem(buildStorageKey(ns, TOKEN_KEY_SUFFIXES.refreshToken));
|
|
227
|
+
const expiresAtStr = storage.getItem(buildStorageKey(ns, TOKEN_KEY_SUFFIXES.expiresAt));
|
|
228
|
+
if (!accessToken || !refreshToken || !expiresAtStr) return null;
|
|
229
|
+
const expiresAt = parseInt(expiresAtStr, 10);
|
|
230
|
+
if (isNaN(expiresAt)) return null;
|
|
231
|
+
return { accessToken, refreshToken, expiresAt };
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function setTokensSyncImpl(ns, tokens) {
|
|
237
|
+
const storage = getSessionStorage();
|
|
238
|
+
if (!storage) return;
|
|
239
|
+
try {
|
|
240
|
+
storage.setItem(buildStorageKey(ns, TOKEN_KEY_SUFFIXES.accessToken), tokens.accessToken);
|
|
241
|
+
storage.setItem(buildStorageKey(ns, TOKEN_KEY_SUFFIXES.refreshToken), tokens.refreshToken);
|
|
242
|
+
storage.setItem(buildStorageKey(ns, TOKEN_KEY_SUFFIXES.expiresAt), String(tokens.expiresAt));
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function clearTokensSyncImpl(ns) {
|
|
247
|
+
const storage = getSessionStorage();
|
|
248
|
+
if (!storage) return;
|
|
249
|
+
try {
|
|
250
|
+
storage.removeItem(buildStorageKey(ns, TOKEN_KEY_SUFFIXES.accessToken));
|
|
251
|
+
storage.removeItem(buildStorageKey(ns, TOKEN_KEY_SUFFIXES.refreshToken));
|
|
252
|
+
storage.removeItem(buildStorageKey(ns, TOKEN_KEY_SUFFIXES.expiresAt));
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function getItemSyncImpl(key) {
|
|
257
|
+
const storage = getSessionStorage();
|
|
258
|
+
if (!storage) return null;
|
|
259
|
+
try {
|
|
260
|
+
return storage.getItem(key);
|
|
261
|
+
} catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function setItemSyncImpl(key, value) {
|
|
266
|
+
const storage = getSessionStorage();
|
|
267
|
+
if (!storage) return;
|
|
268
|
+
try {
|
|
269
|
+
storage.setItem(key, value);
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function removeItemSyncImpl(key) {
|
|
274
|
+
const storage = getSessionStorage();
|
|
275
|
+
if (!storage) return;
|
|
276
|
+
try {
|
|
277
|
+
storage.removeItem(key);
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function clearSyncImpl(ns) {
|
|
282
|
+
const storage = getSessionStorage();
|
|
283
|
+
if (!storage) return;
|
|
284
|
+
try {
|
|
285
|
+
const prefix = ns.tenantId || ns.appId ? `auth_${ns.tenantId || ""}_${ns.appId || ""}_` : getStorageKeyPrefix();
|
|
286
|
+
const keys = [];
|
|
287
|
+
for (let i = 0; i < storage.length; i++) {
|
|
288
|
+
const key = storage.key(i);
|
|
289
|
+
if (key && key.startsWith(prefix)) keys.push(key);
|
|
290
|
+
}
|
|
291
|
+
keys.forEach((k) => storage.removeItem(k));
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/infrastructure/storage/cookie-adapter.ts
|
|
297
|
+
var DEFAULT_COOKIE_OPTIONS = {
|
|
298
|
+
path: "/",
|
|
299
|
+
sameSite: "lax",
|
|
300
|
+
maxAge: 60 * 60 * 24 * 7
|
|
301
|
+
// 7 days for refresh token
|
|
302
|
+
};
|
|
303
|
+
function getDocument() {
|
|
304
|
+
if (typeof document === "undefined") return null;
|
|
305
|
+
return document;
|
|
306
|
+
}
|
|
307
|
+
function getCookie(name) {
|
|
308
|
+
const doc = getDocument();
|
|
309
|
+
if (!doc || !doc.cookie) return null;
|
|
310
|
+
const match = doc.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
|
|
311
|
+
return match ? decodeURIComponent(match[2]) : null;
|
|
312
|
+
}
|
|
313
|
+
function setCookie(name, value, options = {}) {
|
|
314
|
+
const doc = getDocument();
|
|
315
|
+
if (!doc) return;
|
|
316
|
+
const { path = "/", sameSite = "lax", maxAge } = { ...DEFAULT_COOKIE_OPTIONS, ...options };
|
|
317
|
+
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)};path=${path};SameSite=${sameSite}`;
|
|
318
|
+
if (maxAge != null) cookie += `;max-age=${maxAge}`;
|
|
319
|
+
doc.cookie = cookie;
|
|
320
|
+
}
|
|
321
|
+
function deleteCookie(name) {
|
|
322
|
+
const doc = getDocument();
|
|
323
|
+
if (!doc) return;
|
|
324
|
+
doc.cookie = `${encodeURIComponent(name)}=;path=/;max-age=0`;
|
|
325
|
+
}
|
|
326
|
+
function createCookieStorageAdapter(namespace, options) {
|
|
327
|
+
const prefix = options?.cookiePrefix ?? "auth";
|
|
328
|
+
const keyPrefix = namespace.tenantId || namespace.appId ? `auth_${namespace.tenantId || ""}_${namespace.appId || ""}_` : getStorageKeyPrefix();
|
|
329
|
+
const cookieName = (suffix) => `${prefix}_${namespace.tenantId || "t"}_${namespace.appId || "a"}_${suffix}`.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
330
|
+
return {
|
|
331
|
+
async getTokens() {
|
|
332
|
+
const accessToken = getCookie(cookieName(TOKEN_KEY_SUFFIXES.accessToken));
|
|
333
|
+
const refreshToken = getCookie(cookieName(TOKEN_KEY_SUFFIXES.refreshToken));
|
|
334
|
+
const expiresAtStr = getCookie(cookieName(TOKEN_KEY_SUFFIXES.expiresAt));
|
|
335
|
+
if (!accessToken || !refreshToken || !expiresAtStr) return null;
|
|
336
|
+
const expiresAt = parseInt(expiresAtStr, 10);
|
|
337
|
+
if (isNaN(expiresAt)) return null;
|
|
338
|
+
return { accessToken, refreshToken, expiresAt };
|
|
339
|
+
},
|
|
340
|
+
async setTokens(tokens) {
|
|
341
|
+
setCookie(cookieName(TOKEN_KEY_SUFFIXES.accessToken), tokens.accessToken, { maxAge: 60 * 60 });
|
|
342
|
+
setCookie(cookieName(TOKEN_KEY_SUFFIXES.refreshToken), tokens.refreshToken, { maxAge: 60 * 60 * 24 * 7 });
|
|
343
|
+
setCookie(cookieName(TOKEN_KEY_SUFFIXES.expiresAt), String(tokens.expiresAt), { maxAge: 60 * 60 * 24 });
|
|
344
|
+
},
|
|
345
|
+
async clearTokens() {
|
|
346
|
+
deleteCookie(cookieName(TOKEN_KEY_SUFFIXES.accessToken));
|
|
347
|
+
deleteCookie(cookieName(TOKEN_KEY_SUFFIXES.refreshToken));
|
|
348
|
+
deleteCookie(cookieName(TOKEN_KEY_SUFFIXES.expiresAt));
|
|
349
|
+
},
|
|
350
|
+
async getItem(key) {
|
|
351
|
+
const name = key.startsWith(keyPrefix) ? key.slice(keyPrefix.length) : key;
|
|
352
|
+
return getCookie(cookieName(name));
|
|
353
|
+
},
|
|
354
|
+
async setItem(key, value) {
|
|
355
|
+
const name = key.startsWith(keyPrefix) ? key.slice(keyPrefix.length) : key;
|
|
356
|
+
setCookie(cookieName(name), value);
|
|
357
|
+
},
|
|
358
|
+
async removeItem(key) {
|
|
359
|
+
const name = key.startsWith(keyPrefix) ? key.slice(keyPrefix.length) : key;
|
|
360
|
+
deleteCookie(cookieName(name));
|
|
361
|
+
},
|
|
362
|
+
async clear() {
|
|
363
|
+
const suffixes = Object.values(TOKEN_KEY_SUFFIXES);
|
|
364
|
+
suffixes.forEach((s) => deleteCookie(cookieName(s)));
|
|
365
|
+
Object.values(CONFIG_KEY_SUFFIXES).forEach((suffix) => deleteCookie(cookieName(suffix)));
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/infrastructure/storage/kv.ts
|
|
371
|
+
function keyToSuffix(key) {
|
|
372
|
+
const prefix = getStorageKeyPrefix();
|
|
373
|
+
return key.startsWith(prefix) ? key.slice(prefix.length) : key;
|
|
374
|
+
}
|
|
375
|
+
function setStorage(key, value) {
|
|
376
|
+
const adapter = getStorageAdapter();
|
|
377
|
+
if (!adapter?.setItemSync) return;
|
|
378
|
+
const ns = getStorageNamespace();
|
|
379
|
+
const suffix = keyToSuffix(key);
|
|
380
|
+
try {
|
|
381
|
+
adapter.setItemSync(buildStorageKey(ns, suffix), value);
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function getStorage(key) {
|
|
386
|
+
const adapter = getStorageAdapter();
|
|
387
|
+
if (!adapter?.getItemSync) return null;
|
|
388
|
+
const ns = getStorageNamespace();
|
|
389
|
+
const suffix = keyToSuffix(key);
|
|
390
|
+
try {
|
|
391
|
+
return adapter.getItemSync(buildStorageKey(ns, suffix));
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function removeStorage(key) {
|
|
397
|
+
const adapter = getStorageAdapter();
|
|
398
|
+
if (!adapter?.removeItemSync) return;
|
|
399
|
+
const ns = getStorageNamespace();
|
|
400
|
+
const suffix = keyToSuffix(key);
|
|
401
|
+
try {
|
|
402
|
+
adapter.removeItemSync(buildStorageKey(ns, suffix));
|
|
403
|
+
} catch {
|
|
404
|
+
}
|
|
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
|
+
};
|
|
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);
|
|
450
|
+
}
|
|
451
|
+
function clearAuthSessionStorage() {
|
|
452
|
+
clearAuthStorage();
|
|
453
|
+
}
|
|
454
|
+
function clearAllCookies() {
|
|
455
|
+
const runtime = getAuthRuntime();
|
|
456
|
+
try {
|
|
457
|
+
runtime?.clearCookies?.clearCookies();
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function clearAllStorage() {
|
|
462
|
+
clearAuthStorage();
|
|
463
|
+
clearAllCookies();
|
|
464
|
+
}
|
|
465
|
+
function getAccessToken() {
|
|
466
|
+
const adapter = getStorageAdapter();
|
|
467
|
+
if (adapter?.getTokensSync) {
|
|
468
|
+
const t = adapter.getTokensSync();
|
|
469
|
+
return t?.accessToken ?? null;
|
|
470
|
+
}
|
|
471
|
+
return getStorage(STORAGE_KEYS.accessToken);
|
|
472
|
+
}
|
|
473
|
+
function getRefreshToken() {
|
|
474
|
+
const adapter = getStorageAdapter();
|
|
475
|
+
if (adapter?.getTokensSync) {
|
|
476
|
+
const t = adapter.getTokensSync();
|
|
477
|
+
return t?.refreshToken ?? null;
|
|
478
|
+
}
|
|
479
|
+
return getStorage(STORAGE_KEYS.refreshToken);
|
|
480
|
+
}
|
|
481
|
+
function getExpiresAt() {
|
|
482
|
+
const adapter = getStorageAdapter();
|
|
483
|
+
if (adapter?.getTokensSync) {
|
|
484
|
+
const t = adapter.getTokensSync();
|
|
485
|
+
return t?.expiresAt ?? null;
|
|
486
|
+
}
|
|
487
|
+
const expiresAt = getStorage(STORAGE_KEYS.expiresAt);
|
|
488
|
+
if (!expiresAt) return null;
|
|
489
|
+
const timestamp = parseInt(expiresAt, 10);
|
|
490
|
+
return isNaN(timestamp) ? null : timestamp;
|
|
491
|
+
}
|
|
492
|
+
function isAccessTokenExpired() {
|
|
493
|
+
const expiresAt = getExpiresAt();
|
|
494
|
+
if (!expiresAt) return true;
|
|
495
|
+
const buffer = 60 * 1e3;
|
|
496
|
+
return Date.now() >= expiresAt - buffer;
|
|
497
|
+
}
|
|
498
|
+
function getCodeVerifier() {
|
|
499
|
+
return getStorage(STORAGE_KEYS.codeVerifier);
|
|
500
|
+
}
|
|
501
|
+
function getState() {
|
|
502
|
+
return getStorage(STORAGE_KEYS.state);
|
|
503
|
+
}
|
|
504
|
+
function getTenantKey() {
|
|
505
|
+
return getStorage(STORAGE_KEYS.tenantKey);
|
|
506
|
+
}
|
|
507
|
+
function setTenantKey(tenantKey) {
|
|
508
|
+
setStorage(STORAGE_KEYS.tenantKey, tenantKey);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/utils/logger.ts
|
|
512
|
+
function isDev() {
|
|
513
|
+
if (typeof window === "undefined") {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
const hostname = window.location.hostname;
|
|
517
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.endsWith(".local");
|
|
518
|
+
}
|
|
519
|
+
var Logger = class {
|
|
520
|
+
constructor() {
|
|
521
|
+
this.prefix = "[tenneo-auth-gateway-plugin]";
|
|
522
|
+
}
|
|
523
|
+
log(level, message, data) {
|
|
524
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
525
|
+
const logMessage = `${this.prefix} [${timestamp}] [${level.toUpperCase()}] ${message}`;
|
|
526
|
+
switch (level) {
|
|
527
|
+
case "info":
|
|
528
|
+
console.log(logMessage, data || "");
|
|
529
|
+
break;
|
|
530
|
+
case "warn":
|
|
531
|
+
console.warn(logMessage, data || "");
|
|
532
|
+
break;
|
|
533
|
+
case "error":
|
|
534
|
+
console.error(logMessage, data || "");
|
|
535
|
+
break;
|
|
536
|
+
case "debug":
|
|
537
|
+
if (isDev()) {
|
|
538
|
+
console.debug(logMessage, data || "");
|
|
539
|
+
}
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
info(message, data) {
|
|
544
|
+
this.log("info", message, data);
|
|
545
|
+
}
|
|
546
|
+
warn(message, data) {
|
|
547
|
+
this.log("warn", message, data);
|
|
548
|
+
}
|
|
549
|
+
error(message, data) {
|
|
550
|
+
this.log("error", message, data);
|
|
551
|
+
}
|
|
552
|
+
debug(message, data) {
|
|
553
|
+
this.log("debug", message, data);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
var logger = new Logger();
|
|
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
|
+
|
|
804
|
+
// src/domain/errors.ts
|
|
805
|
+
function isLocalhostHostname(hostname) {
|
|
806
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname.startsWith("192.168.") || hostname.startsWith("10.") || hostname.startsWith("172.16.");
|
|
807
|
+
}
|
|
808
|
+
function isDev2() {
|
|
809
|
+
if (typeof window === "undefined") return false;
|
|
810
|
+
return isLocalhostHostname(window.location.hostname);
|
|
811
|
+
}
|
|
812
|
+
function enforceHttps(allowLocalhost = true) {
|
|
813
|
+
if (typeof window === "undefined") return;
|
|
814
|
+
const isLocalhostWindow = window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1" || window.location.hostname.startsWith("192.168.") || window.location.hostname.startsWith("10.") || window.location.hostname.startsWith("172.16.");
|
|
815
|
+
const isHttp = window.location.protocol === "http:";
|
|
816
|
+
if (allowLocalhost && isLocalhostWindow && isHttp) {
|
|
817
|
+
logger.debug("HTTP allowed on localhost for development");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (isHttp && !isLocalhostWindow) {
|
|
821
|
+
const error = new Error("HTTPS is required for authentication. Please use HTTPS to access this application.");
|
|
822
|
+
logger.error("HTTPS enforcement failed", {
|
|
823
|
+
protocol: window.location.protocol,
|
|
824
|
+
hostname: window.location.hostname
|
|
825
|
+
});
|
|
826
|
+
throw error;
|
|
827
|
+
}
|
|
828
|
+
logger.debug("HTTPS check passed", { protocol: window.location.protocol });
|
|
829
|
+
}
|
|
830
|
+
function sanitizeErrorMessage(error, genericMessage) {
|
|
831
|
+
if (isDev2()) {
|
|
832
|
+
if (error instanceof Error) return error.message;
|
|
833
|
+
return String(error);
|
|
834
|
+
}
|
|
835
|
+
return genericMessage;
|
|
836
|
+
}
|
|
837
|
+
var SanitizedError = class extends Error {
|
|
838
|
+
constructor(code, genericMessage, originalError) {
|
|
839
|
+
super(genericMessage);
|
|
840
|
+
this.name = "SanitizedError";
|
|
841
|
+
this.code = code;
|
|
842
|
+
if (isDev2() && originalError instanceof Error) {
|
|
843
|
+
this.originalMessage = originalError.message;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// src/domain/auth-lifecycle.ts
|
|
849
|
+
function resolveAuthLifecycleState(input) {
|
|
850
|
+
const {
|
|
851
|
+
url,
|
|
852
|
+
getStorage: getStorage3,
|
|
853
|
+
isCallbackUrl: isCallbackUrl2,
|
|
854
|
+
hasValidToken,
|
|
855
|
+
hasToken,
|
|
856
|
+
hasRefreshToken,
|
|
857
|
+
isTokenExpired
|
|
858
|
+
} = input;
|
|
859
|
+
const callbackProcessed = getStorage3("callback_processed") === "true";
|
|
860
|
+
const tenantSwitched = getStorage3("tenant_switched") === "true";
|
|
861
|
+
if (isCallbackUrl2(url)) return "PROCESSING_CALLBACK" /* PROCESSING_CALLBACK */;
|
|
862
|
+
if (hasValidToken) return "AUTHENTICATED" /* AUTHENTICATED */;
|
|
863
|
+
if (callbackProcessed && !hasToken) return "LOGIN_REQUIRED" /* LOGIN_REQUIRED */;
|
|
864
|
+
if (hasToken && isTokenExpired && hasRefreshToken) return "TOKEN_EXPIRED" /* TOKEN_EXPIRED */;
|
|
865
|
+
if (hasToken && isTokenExpired && !hasRefreshToken) return "LOGIN_REQUIRED" /* LOGIN_REQUIRED */;
|
|
866
|
+
if (tenantSwitched) return "TENANT_SWITCH" /* TENANT_SWITCH */;
|
|
867
|
+
return "LOGIN_REQUIRED" /* LOGIN_REQUIRED */;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/application/lifecycle-state.ts
|
|
871
|
+
var BOOTSTRAP_STALE_MS = 5e3;
|
|
872
|
+
var bootstrapInProgress = false;
|
|
873
|
+
var bootstrapTimestamp = 0;
|
|
874
|
+
var callbackProcessing = false;
|
|
875
|
+
var flowInProgress = false;
|
|
876
|
+
var flowTimestamp = 0;
|
|
877
|
+
function isBootstrapInProgress() {
|
|
878
|
+
if (!bootstrapInProgress) return false;
|
|
879
|
+
if (!bootstrapTimestamp) return true;
|
|
880
|
+
return Date.now() - bootstrapTimestamp < BOOTSTRAP_STALE_MS;
|
|
881
|
+
}
|
|
882
|
+
function setBootstrapInProgress() {
|
|
883
|
+
bootstrapInProgress = true;
|
|
884
|
+
bootstrapTimestamp = Date.now();
|
|
885
|
+
}
|
|
886
|
+
function clearBootstrapInProgress() {
|
|
887
|
+
bootstrapInProgress = false;
|
|
888
|
+
bootstrapTimestamp = 0;
|
|
889
|
+
}
|
|
890
|
+
function isCallbackProcessing() {
|
|
891
|
+
return callbackProcessing;
|
|
892
|
+
}
|
|
893
|
+
function setCallbackProcessing() {
|
|
894
|
+
callbackProcessing = true;
|
|
895
|
+
}
|
|
896
|
+
function clearCallbackProcessing() {
|
|
897
|
+
callbackProcessing = false;
|
|
898
|
+
}
|
|
899
|
+
function isFlowInProgress() {
|
|
900
|
+
return flowInProgress;
|
|
901
|
+
}
|
|
902
|
+
function setFlowInProgress() {
|
|
903
|
+
flowInProgress = true;
|
|
904
|
+
flowTimestamp = Date.now();
|
|
905
|
+
}
|
|
906
|
+
function clearFlowInProgress() {
|
|
907
|
+
flowInProgress = false;
|
|
908
|
+
flowTimestamp = 0;
|
|
909
|
+
}
|
|
910
|
+
function isFlowFlagStale(thresholdMs = 3e3) {
|
|
911
|
+
if (!flowTimestamp) return true;
|
|
912
|
+
return Date.now() - flowTimestamp > thresholdMs;
|
|
913
|
+
}
|
|
914
|
+
function clearFlowFlags() {
|
|
915
|
+
clearFlowInProgress();
|
|
916
|
+
}
|
|
917
|
+
function setFlowFlags() {
|
|
918
|
+
setFlowInProgress();
|
|
919
|
+
}
|
|
920
|
+
|
|
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;
|
|
943
|
+
}
|
|
944
|
+
function getWildcardHandlers() {
|
|
945
|
+
let set = listeners.get("*");
|
|
946
|
+
if (!set) {
|
|
947
|
+
set = /* @__PURE__ */ new Set();
|
|
948
|
+
listeners.set("*", set);
|
|
949
|
+
}
|
|
950
|
+
return set;
|
|
951
|
+
}
|
|
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);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// src/config/constants.ts
|
|
976
|
+
var OAUTH_CONSTANTS = {
|
|
977
|
+
SCOPES: "openid profile email",
|
|
978
|
+
RESPONSE_TYPE: "code",
|
|
979
|
+
CODE_CHALLENGE_METHOD: "S256",
|
|
980
|
+
TOKEN_GRANT_TYPE: "authorization_code",
|
|
981
|
+
REFRESH_GRANT_TYPE: "refresh_token"
|
|
982
|
+
};
|
|
983
|
+
var STORAGE_KEY_PREFIX = "tenneo_auth_";
|
|
984
|
+
var STORAGE_KEYS2 = {
|
|
985
|
+
authServerUrl: `${STORAGE_KEY_PREFIX}auth_server_url`,
|
|
986
|
+
clientId: `${STORAGE_KEY_PREFIX}client_id`,
|
|
987
|
+
redirectUri: `${STORAGE_KEY_PREFIX}redirect_uri`,
|
|
988
|
+
tenantId: `${STORAGE_KEY_PREFIX}tenant_id`,
|
|
989
|
+
codeVerifier: `${STORAGE_KEY_PREFIX}code_verifier`,
|
|
990
|
+
state: `${STORAGE_KEY_PREFIX}state`,
|
|
991
|
+
accessToken: `${STORAGE_KEY_PREFIX}access_token`,
|
|
992
|
+
refreshToken: `${STORAGE_KEY_PREFIX}refresh_token`,
|
|
993
|
+
expiresAt: `${STORAGE_KEY_PREFIX}expires_at`
|
|
994
|
+
};
|
|
995
|
+
OAUTH_CONSTANTS.SCOPES;
|
|
996
|
+
var DEFAULT_RESPONSE_TYPE = OAUTH_CONSTANTS.RESPONSE_TYPE;
|
|
997
|
+
var DEFAULT_CODE_CHALLENGE_METHOD = OAUTH_CONSTANTS.CODE_CHALLENGE_METHOD;
|
|
998
|
+
var DEFAULT_TOKEN_GRANT_TYPE = OAUTH_CONSTANTS.TOKEN_GRANT_TYPE;
|
|
999
|
+
var DEFAULT_REFRESH_GRANT_TYPE = OAUTH_CONSTANTS.REFRESH_GRANT_TYPE;
|
|
1000
|
+
|
|
1001
|
+
// src/config/index.ts
|
|
1002
|
+
function getAuthConfig2(options) {
|
|
1003
|
+
return getAuthConfig(options);
|
|
1004
|
+
}
|
|
1005
|
+
function requireAuthConfig(overrides) {
|
|
1006
|
+
return getAuthConfig({ strict: true, overrides });
|
|
1007
|
+
}
|
|
1008
|
+
function getConfig() {
|
|
1009
|
+
return resolveConfig();
|
|
1010
|
+
}
|
|
1011
|
+
function getRedirectUri2() {
|
|
1012
|
+
const cfg = resolveConfig();
|
|
1013
|
+
if (cfg.callbackUrl) return cfg.callbackUrl;
|
|
1014
|
+
if (typeof window !== "undefined") return `${window.location.origin}/callback`;
|
|
1015
|
+
return "";
|
|
1016
|
+
}
|
|
1017
|
+
var Config = {
|
|
1018
|
+
getApiBaseUrl() {
|
|
1019
|
+
return resolveConfig().apiBaseUrl;
|
|
1020
|
+
},
|
|
1021
|
+
getAuthServerUrl() {
|
|
1022
|
+
return resolveConfig().authServerUrl;
|
|
1023
|
+
},
|
|
1024
|
+
getClientCode() {
|
|
1025
|
+
return resolveConfig().clientCode;
|
|
1026
|
+
},
|
|
1027
|
+
getRedirectUri() {
|
|
1028
|
+
return getRedirectUri2();
|
|
1029
|
+
},
|
|
1030
|
+
getAll() {
|
|
1031
|
+
return resolveConfig();
|
|
1032
|
+
}
|
|
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
|
+
}
|
|
1043
|
+
|
|
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;
|
|
1051
|
+
}
|
|
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;
|
|
1058
|
+
}
|
|
1059
|
+
return entry.config;
|
|
1060
|
+
}
|
|
1061
|
+
set(tenantKey, config) {
|
|
1062
|
+
this.cache.set(tenantKey, {
|
|
1063
|
+
config,
|
|
1064
|
+
expiresAt: Date.now() + this.ttlMs
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
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
|
+
}
|
|
1075
|
+
}
|
|
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;
|
|
1083
|
+
}
|
|
1084
|
+
const existing = this.inFlight.get(key);
|
|
1085
|
+
if (existing) {
|
|
1086
|
+
logger.debug("Tenant resolution reused (in-flight)", { tenantKey: key });
|
|
1087
|
+
return existing;
|
|
1088
|
+
}
|
|
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;
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
// src/infrastructure/tenant/tenantResolution.ts
|
|
1104
|
+
var defaultTenantConfigCache = new TenantConfigCache();
|
|
1105
|
+
function consumePropsConfig() {
|
|
1106
|
+
const propsConfigKey = getStorageKey("props_config");
|
|
1107
|
+
try {
|
|
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);
|
|
1116
|
+
}
|
|
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;
|
|
1129
|
+
}
|
|
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");
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/strategies/oauth2-pkce/token-exchange.ts
|
|
1168
|
+
function storeTokens(tokenData) {
|
|
1169
|
+
const expiresIn = tokenData.expires_in || 3600;
|
|
1170
|
+
const expiresAt = Date.now() + expiresIn * 1e3;
|
|
1171
|
+
const adapter = getStorageAdapter();
|
|
1172
|
+
if (adapter?.setTokensSync) {
|
|
1173
|
+
adapter.setTokensSync({
|
|
1174
|
+
accessToken: tokenData.access_token,
|
|
1175
|
+
refreshToken: tokenData.refresh_token,
|
|
1176
|
+
expiresAt
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
setStorage(STORAGE_KEYS.accessToken, tokenData.access_token);
|
|
1180
|
+
setStorage(STORAGE_KEYS.refreshToken, tokenData.refresh_token);
|
|
1181
|
+
setStorage(STORAGE_KEYS.expiresAt, expiresAt.toString());
|
|
1182
|
+
if (isLocalhost2()) {
|
|
1183
|
+
logger.debug("Tokens stored successfully", {
|
|
1184
|
+
expiresAt: new Date(expiresAt).toISOString(),
|
|
1185
|
+
expiresIn
|
|
1186
|
+
});
|
|
1187
|
+
} else {
|
|
1188
|
+
logger.info("Tokens stored successfully");
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
async function exchangeCodeForTokens(params) {
|
|
1192
|
+
try {
|
|
1193
|
+
const { code, codeVerifier, clientId, redirectUri, tenantId } = params;
|
|
1194
|
+
const apiAuthBaseUrl = Config.getAuthServerUrl();
|
|
1195
|
+
const tokenUrl = `${apiAuthBaseUrl}/oauth/token`;
|
|
1196
|
+
const body = new URLSearchParams({
|
|
1197
|
+
grant_type: DEFAULT_TOKEN_GRANT_TYPE,
|
|
1198
|
+
code,
|
|
1199
|
+
client_id: `public-${clientId}`,
|
|
1200
|
+
redirect_uri: redirectUri,
|
|
1201
|
+
code_verifier: codeVerifier
|
|
1202
|
+
});
|
|
1203
|
+
const response = await fetch(tokenUrl, {
|
|
1204
|
+
method: "POST",
|
|
1205
|
+
headers: {
|
|
1206
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1207
|
+
"X-Tenant-Id": tenantId
|
|
1208
|
+
},
|
|
1209
|
+
body: body.toString()
|
|
1210
|
+
});
|
|
1211
|
+
if (!response.ok) {
|
|
1212
|
+
const responseData = await response.json().catch(() => ({}));
|
|
1213
|
+
logger.error("Token exchange failed", {
|
|
1214
|
+
status: response.status,
|
|
1215
|
+
statusText: response.statusText,
|
|
1216
|
+
responseData,
|
|
1217
|
+
requestUrl: tokenUrl
|
|
1218
|
+
});
|
|
1219
|
+
throw new Error(
|
|
1220
|
+
`Token exchange failed: ${response.status} ${response.statusText}${Object.keys(responseData).length ? ` - ${JSON.stringify(responseData)}` : ""}`
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
const tokenData = await response.json();
|
|
1224
|
+
if (!tokenData.access_token || !tokenData.refresh_token || !tokenData.expires_in) {
|
|
1225
|
+
logger.error("Invalid token response received");
|
|
1226
|
+
throw new Error("Invalid token response");
|
|
1227
|
+
}
|
|
1228
|
+
storeTokens(tokenData);
|
|
1229
|
+
return tokenData;
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
const isAborted = error instanceof Error && error.name === "AbortError";
|
|
1232
|
+
if (isAborted) {
|
|
1233
|
+
logger.warn("Token exchange request canceled (likely due to navigation or component unmount)");
|
|
1234
|
+
const existingToken = getAccessToken();
|
|
1235
|
+
if (existingToken && !isAccessTokenExpired()) {
|
|
1236
|
+
return {
|
|
1237
|
+
access_token: existingToken,
|
|
1238
|
+
refresh_token: getRefreshToken() || "",
|
|
1239
|
+
expires_in: 3600
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
throw new Error("Token exchange request was canceled");
|
|
1243
|
+
}
|
|
1244
|
+
logger.error("Token exchange failed", {
|
|
1245
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1246
|
+
});
|
|
1247
|
+
throw error;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
var refreshPromise = null;
|
|
1251
|
+
async function refreshAccessToken() {
|
|
1252
|
+
if (refreshPromise) return refreshPromise;
|
|
1253
|
+
refreshPromise = (async () => {
|
|
1254
|
+
try {
|
|
1255
|
+
const refreshToken = getRefreshToken();
|
|
1256
|
+
const clientId = getClientId();
|
|
1257
|
+
const tenantId = getTenantId();
|
|
1258
|
+
logger.debug("Token refresh - collected data", {
|
|
1259
|
+
hasRefreshToken: !!refreshToken,
|
|
1260
|
+
clientId,
|
|
1261
|
+
tenantId
|
|
1262
|
+
});
|
|
1263
|
+
if (!refreshToken || !clientId || !tenantId) {
|
|
1264
|
+
logger.error("Token refresh failed: missing required data", {
|
|
1265
|
+
hasRefreshToken: !!refreshToken,
|
|
1266
|
+
hasClientId: !!clientId,
|
|
1267
|
+
hasTenantId: !!tenantId
|
|
1268
|
+
});
|
|
1269
|
+
throw new Error("Cannot refresh token: missing required data");
|
|
1270
|
+
}
|
|
1271
|
+
const refreshTokenApiAuthBaseUrl = Config.getAuthServerUrl();
|
|
1272
|
+
const refreshtokenUrl = `${refreshTokenApiAuthBaseUrl}/oauth/refresh`;
|
|
1273
|
+
const body = new URLSearchParams({
|
|
1274
|
+
grant_type: DEFAULT_REFRESH_GRANT_TYPE,
|
|
1275
|
+
refresh_token: refreshToken,
|
|
1276
|
+
client_id: clientId.startsWith("public-") ? clientId : `public-${clientId}`,
|
|
1277
|
+
tenant_id: tenantId
|
|
1278
|
+
});
|
|
1279
|
+
const response = await fetch(refreshtokenUrl, {
|
|
1280
|
+
method: "POST",
|
|
1281
|
+
headers: {
|
|
1282
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1283
|
+
"Accept": "application/json",
|
|
1284
|
+
"X-Tenant-Id": tenantId
|
|
1285
|
+
},
|
|
1286
|
+
body: body.toString()
|
|
1287
|
+
});
|
|
1288
|
+
if (!response.ok) {
|
|
1289
|
+
const responseData = await response.json().catch(() => ({}));
|
|
1290
|
+
const isExpiredOrInvalidGrant = response.status === 401 || responseData?.error === "invalid_grant" || typeof responseData?.error_description === "string" && (responseData.error_description.includes("expired") || responseData.error_description.includes("invalid"));
|
|
1291
|
+
if (isExpiredOrInvalidGrant) {
|
|
1292
|
+
removeStorage(STORAGE_KEYS.refreshToken);
|
|
1293
|
+
removeStorage(STORAGE_KEYS.accessToken);
|
|
1294
|
+
removeStorage(STORAGE_KEYS.expiresAt);
|
|
1295
|
+
emitAuthEvent(AuthEventNames.TOKEN_REFRESH_FAILED, {
|
|
1296
|
+
error: "Refresh token expired or invalid",
|
|
1297
|
+
message: String(responseData?.error_description ?? responseData?.error ?? "Token refresh failed")
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
logger.error("Token refresh failed", {
|
|
1301
|
+
status: response.status,
|
|
1302
|
+
statusText: response.statusText,
|
|
1303
|
+
responseData,
|
|
1304
|
+
requestUrl: refreshtokenUrl
|
|
1305
|
+
});
|
|
1306
|
+
throw new Error(
|
|
1307
|
+
`Token refresh failed: ${response.status} ${response.statusText}${Object.keys(responseData).length ? ` - ${JSON.stringify(responseData)}` : ""}`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
const tokenData = await response.json();
|
|
1311
|
+
if (!tokenData.access_token || !tokenData.refresh_token) {
|
|
1312
|
+
logger.error("Invalid refresh token response received");
|
|
1313
|
+
throw new Error("Invalid refresh token response");
|
|
1314
|
+
}
|
|
1315
|
+
storeTokens(tokenData);
|
|
1316
|
+
emitAuthEvent(AuthEventNames.TOKEN_REFRESH_SUCCESS);
|
|
1317
|
+
return tokenData.access_token;
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
if (error instanceof Error) {
|
|
1320
|
+
logger.error("Token refresh failed", { message: error.message });
|
|
1321
|
+
emitAuthEvent(AuthEventNames.TOKEN_REFRESH_FAILED, { error: error.message });
|
|
1322
|
+
}
|
|
1323
|
+
throw error;
|
|
1324
|
+
} finally {
|
|
1325
|
+
refreshPromise = null;
|
|
1326
|
+
}
|
|
1327
|
+
})();
|
|
1328
|
+
return refreshPromise;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/strategies/oauth2-pkce/pkce.ts
|
|
1332
|
+
function generateCodeVerifier(length = 128) {
|
|
1333
|
+
const array = new Uint8Array(length);
|
|
1334
|
+
crypto.getRandomValues(array);
|
|
1335
|
+
const base64 = btoa(String.fromCharCode(...array));
|
|
1336
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1337
|
+
}
|
|
1338
|
+
async function generateCodeChallenge(verifier) {
|
|
1339
|
+
try {
|
|
1340
|
+
const encoder = new TextEncoder();
|
|
1341
|
+
const data = encoder.encode(verifier);
|
|
1342
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
1343
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1344
|
+
const base64 = btoa(String.fromCharCode(...hashArray));
|
|
1345
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1346
|
+
} catch {
|
|
1347
|
+
throw new Error("Failed to generate code challenge");
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function generateState() {
|
|
1351
|
+
const array = new Uint8Array(32);
|
|
1352
|
+
crypto.getRandomValues(array);
|
|
1353
|
+
const base64 = btoa(String.fromCharCode(...array));
|
|
1354
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// src/utils/validation.ts
|
|
1358
|
+
function isValidGuid(guid) {
|
|
1359
|
+
if (!guid || typeof guid !== "string") {
|
|
1360
|
+
return false;
|
|
1361
|
+
}
|
|
1362
|
+
const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1363
|
+
return guidRegex.test(guid.trim());
|
|
1364
|
+
}
|
|
1365
|
+
function validateTenantId(tenantId) {
|
|
1366
|
+
if (!tenantId || tenantId.trim() === "") {
|
|
1367
|
+
return {
|
|
1368
|
+
isValid: false,
|
|
1369
|
+
error: "Tenant ID is required"
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
if (!isValidGuid(tenantId)) {
|
|
1373
|
+
return {
|
|
1374
|
+
isValid: false,
|
|
1375
|
+
error: "Tenant ID must be a valid GUID format (00000000-0000-0000-0000-000000000001)"
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
return { isValid: true };
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// src/application/security/redirectLoopGuard.ts
|
|
1382
|
+
var DEFAULT_MAX_REDIRECTS = 5;
|
|
1383
|
+
var WINDOW_MS = 60 * 1e3;
|
|
1384
|
+
var redirectCount = 0;
|
|
1385
|
+
var firstRedirectAt = 0;
|
|
1386
|
+
function resetRedirectLoopGuard() {
|
|
1387
|
+
redirectCount = 0;
|
|
1388
|
+
firstRedirectAt = 0;
|
|
1389
|
+
}
|
|
1390
|
+
function checkAndIncrementRedirect(maxAttempts = DEFAULT_MAX_REDIRECTS) {
|
|
1391
|
+
const now = Date.now();
|
|
1392
|
+
if (now - firstRedirectAt > WINDOW_MS) {
|
|
1393
|
+
redirectCount = 0;
|
|
1394
|
+
firstRedirectAt = now;
|
|
1395
|
+
}
|
|
1396
|
+
if (redirectCount >= maxAttempts) {
|
|
1397
|
+
emitAuthEvent(AuthEventNames.REDIRECT_LOOP_RISK, {
|
|
1398
|
+
message: `Redirect attempt limit (${maxAttempts}) exceeded`
|
|
1399
|
+
});
|
|
1400
|
+
return false;
|
|
1401
|
+
}
|
|
1402
|
+
redirectCount++;
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/strategies/oauth2-pkce/oauth.ts
|
|
1407
|
+
function storeTenantConfig(config) {
|
|
1408
|
+
const validation = validateTenantId(config.tenantId);
|
|
1409
|
+
if (!validation.isValid) {
|
|
1410
|
+
logger.warn("Tenant ID validation failed", { tenantId: config.tenantId });
|
|
1411
|
+
}
|
|
1412
|
+
const authServerToStore = Config.getAuthServerUrl() || config.authServer;
|
|
1413
|
+
const redirectUriToStore = Config.getRedirectUri();
|
|
1414
|
+
writeHostConfigCache({
|
|
1415
|
+
authServerUrl: authServerToStore,
|
|
1416
|
+
clientId: config.client.clientId,
|
|
1417
|
+
clientCode: config.client.clientCode ?? config.client.clientId,
|
|
1418
|
+
redirectUri: redirectUriToStore,
|
|
1419
|
+
callbackUrl: redirectUriToStore,
|
|
1420
|
+
tenantId: config.tenantId,
|
|
1421
|
+
tenantKey: config.tenantKey ?? "",
|
|
1422
|
+
tenantCode: config.tenantKey ?? ""
|
|
1423
|
+
});
|
|
1424
|
+
if (config.tenantKey) {
|
|
1425
|
+
setStorage(STORAGE_KEYS.tenantKey, config.tenantKey);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async function initiateAuthFlow(config) {
|
|
1429
|
+
try {
|
|
1430
|
+
logger.info("Initiating OAuth authorization flow");
|
|
1431
|
+
const authServerFromProps = Config.getAuthServerUrl();
|
|
1432
|
+
const authServerEffective = (authServerFromProps || config?.authServer || "").replace(/\/+$/, "");
|
|
1433
|
+
const redirectUriEffective = Config.getRedirectUri();
|
|
1434
|
+
const tenantIdEffective = config?.tenantId || "";
|
|
1435
|
+
const clientIdEffective = config?.client?.clientId || "";
|
|
1436
|
+
logger.debug("[initiateAuthFlow] Resolved inputs", {
|
|
1437
|
+
authServerFromProps: authServerFromProps ? "(provided)" : "(empty)",
|
|
1438
|
+
authServerEffective,
|
|
1439
|
+
redirectUriEffective,
|
|
1440
|
+
tenantIdEffective,
|
|
1441
|
+
clientIdEffective,
|
|
1442
|
+
scopes: config?.client?.scopes,
|
|
1443
|
+
hasRuntime: !!getAuthRuntime()
|
|
1444
|
+
});
|
|
1445
|
+
logger.info("[initiateAuthFlow] authServerUrl for OAuth", {
|
|
1446
|
+
authServerFromProps: authServerFromProps || null,
|
|
1447
|
+
authServerEffective,
|
|
1448
|
+
redirectUriEffective,
|
|
1449
|
+
tenantId: tenantIdEffective,
|
|
1450
|
+
tenantKey: config.tenantKey ?? null,
|
|
1451
|
+
clientIdEffective
|
|
1452
|
+
});
|
|
1453
|
+
const token = getAccessToken();
|
|
1454
|
+
if (token && !isAccessTokenExpired()) {
|
|
1455
|
+
logger.debug("Valid token already exists, skipping auth flow");
|
|
1456
|
+
clearFlowFlags();
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (isCallbackUrl()) {
|
|
1460
|
+
logger.debug("Callback URL detected - clearing flow flag to allow callback processing");
|
|
1461
|
+
clearFlowFlags();
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
if (isFlowInProgress()) {
|
|
1465
|
+
if (isFlowFlagStale(3e3)) {
|
|
1466
|
+
logger.info("Stale auth flow flag detected, clearing and proceeding");
|
|
1467
|
+
clearFlowFlags();
|
|
1468
|
+
} else {
|
|
1469
|
+
logger.debug("Auth flow already in progress (recent), skipping duplicate redirect");
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
setFlowFlags();
|
|
1474
|
+
if (!(Config.getAuthServerUrl() || config?.authServer) || !config.client?.clientId || !config.client?.redirectUri) {
|
|
1475
|
+
logger.error("[initiateAuthFlow] Missing required tenant config", {
|
|
1476
|
+
authServerEffective,
|
|
1477
|
+
clientId: config?.client?.clientId,
|
|
1478
|
+
redirectUriFromTenantApi: config?.client?.redirectUri,
|
|
1479
|
+
redirectUriEffective
|
|
1480
|
+
});
|
|
1481
|
+
throw new Error("Invalid tenant configuration for auth flow");
|
|
1482
|
+
}
|
|
1483
|
+
storeTenantConfig(config);
|
|
1484
|
+
logger.debug("[initiateAuthFlow] Stored tenant config, generating PKCE");
|
|
1485
|
+
removeStorage(getStorageKey("callback_processed"));
|
|
1486
|
+
removeStorage(getStorageKey("state_mismatch"));
|
|
1487
|
+
clearCallbackProcessing();
|
|
1488
|
+
const codeVerifier = generateCodeVerifier();
|
|
1489
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
1490
|
+
const state = generateState();
|
|
1491
|
+
removeStorage(STORAGE_KEYS.codeVerifier);
|
|
1492
|
+
removeStorage(STORAGE_KEYS.state);
|
|
1493
|
+
setStorage(STORAGE_KEYS.codeVerifier, codeVerifier);
|
|
1494
|
+
setStorage(STORAGE_KEYS.state, state);
|
|
1495
|
+
const authUrl = buildAuthorizationUrl(config, codeChallenge, state);
|
|
1496
|
+
logger.debug("[initiateAuthFlow] Built authorization URL", {
|
|
1497
|
+
authServerEffective,
|
|
1498
|
+
urlPrefix: authUrl?.slice(0, 80)
|
|
1499
|
+
});
|
|
1500
|
+
if (!authUrl || !(authUrl.startsWith("http://") || authUrl.startsWith("https://"))) {
|
|
1501
|
+
throw new Error("Invalid authorization URL generated");
|
|
1502
|
+
}
|
|
1503
|
+
if (!checkAndIncrementRedirect()) {
|
|
1504
|
+
logger.warn("Redirect loop protection: max redirect attempts exceeded");
|
|
1505
|
+
clearFlowFlags();
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
logger.info("Redirecting to authorization server");
|
|
1509
|
+
emitAuthEvent(AuthEventNames.LOGIN_STARTED, { tenantKey: config.tenantKey });
|
|
1510
|
+
const runtime = getAuthRuntime();
|
|
1511
|
+
if (runtime) {
|
|
1512
|
+
logger.debug("[initiateAuthFlow] Redirect via runtime.urlProvider.redirect");
|
|
1513
|
+
runtime.urlProvider.redirect(authUrl);
|
|
1514
|
+
} else if (typeof window !== "undefined") {
|
|
1515
|
+
logger.debug("[initiateAuthFlow] Redirect via window.location.replace");
|
|
1516
|
+
window.location.replace(authUrl);
|
|
1517
|
+
} else {
|
|
1518
|
+
logger.error("[initiateAuthFlow] No runtime and no window; cannot redirect", { authUrl });
|
|
1519
|
+
}
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
clearFlowFlags();
|
|
1522
|
+
logger.error("Failed to initiate auth flow", {
|
|
1523
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1524
|
+
});
|
|
1525
|
+
throw error;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function buildAuthorizationUrl(config, codeChallenge, state) {
|
|
1529
|
+
const authServerBase = (Config.getAuthServerUrl() || config.authServer || "").replace(/\/+$/, "");
|
|
1530
|
+
if (isLocalhost2()) {
|
|
1531
|
+
logger.debug("Building authorization URL", {
|
|
1532
|
+
authServer: authServerBase,
|
|
1533
|
+
clientId: config.client.clientId,
|
|
1534
|
+
redirectUri: config.client.redirectUri,
|
|
1535
|
+
scopes: config.client.scopes
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
const tenantIdToUse = config.tenantId;
|
|
1539
|
+
const clientIdToUse = config.client.clientId;
|
|
1540
|
+
const redirectUriToUse = Config.getRedirectUri();
|
|
1541
|
+
const params = new URLSearchParams({
|
|
1542
|
+
client_id: `public-${clientIdToUse}`,
|
|
1543
|
+
tenant_id: tenantIdToUse,
|
|
1544
|
+
redirect_uri: redirectUriToUse,
|
|
1545
|
+
response_type: DEFAULT_RESPONSE_TYPE,
|
|
1546
|
+
scope: config.client.scopes,
|
|
1547
|
+
state,
|
|
1548
|
+
code_challenge: codeChallenge,
|
|
1549
|
+
code_challenge_method: DEFAULT_CODE_CHALLENGE_METHOD
|
|
1550
|
+
});
|
|
1551
|
+
return `${authServerBase}/connect/authorize?${params.toString()}`;
|
|
1552
|
+
}
|
|
1553
|
+
function isProcessed() {
|
|
1554
|
+
return getStorage(getStorageKey("callback_processed")) === "true";
|
|
1555
|
+
}
|
|
1556
|
+
function isProcessing() {
|
|
1557
|
+
return isCallbackProcessing();
|
|
1558
|
+
}
|
|
1559
|
+
function markProcessing() {
|
|
1560
|
+
setCallbackProcessing();
|
|
1561
|
+
}
|
|
1562
|
+
function markProcessed() {
|
|
1563
|
+
setStorage(getStorageKey("callback_processed"), "true");
|
|
1564
|
+
clearCallbackProcessing();
|
|
1565
|
+
}
|
|
1566
|
+
function clearProcessing() {
|
|
1567
|
+
clearCallbackProcessing();
|
|
1568
|
+
removeStorage(getStorageKey("callback_processed"));
|
|
1569
|
+
}
|
|
1570
|
+
var urlCleaned = false;
|
|
1571
|
+
function cleanUrlSoft() {
|
|
1572
|
+
if (urlCleaned) return;
|
|
1573
|
+
if (isProcessing()) return;
|
|
1574
|
+
const runtime = getAuthRuntime();
|
|
1575
|
+
const clean = runtime ? `${runtime.urlProvider.getCurrentUrl().origin}${runtime.urlProvider.getCurrentUrl().pathname}` : typeof window !== "undefined" ? `${window.location.origin}${window.location.pathname}` : "/";
|
|
1576
|
+
if (runtime) runtime.urlProvider.replaceState(clean);
|
|
1577
|
+
else if (typeof window !== "undefined") window.history.replaceState({}, typeof document !== "undefined" ? document.title : "", clean);
|
|
1578
|
+
urlCleaned = true;
|
|
1579
|
+
}
|
|
1580
|
+
function clearAllAuthFlags() {
|
|
1581
|
+
clearFlowFlags();
|
|
1582
|
+
}
|
|
1583
|
+
async function tryRecoverOAuthConfigFromTenant() {
|
|
1584
|
+
const runtime = getAuthRuntime();
|
|
1585
|
+
const tenantKey = (resolveTenantKey() ?? getTenantKey() ?? "").trim();
|
|
1586
|
+
if (!runtime?.tenantResolver || !tenantKey) {
|
|
1587
|
+
logger.debug("[Callback] OAuth config recovery skipped \u2014 no tenantResolver or tenantKey", {
|
|
1588
|
+
hasResolver: !!runtime?.tenantResolver,
|
|
1589
|
+
tenantKey: tenantKey || "(empty)"
|
|
1590
|
+
});
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
try {
|
|
1594
|
+
const tc = await runtime.tenantResolver.resolve(tenantKey);
|
|
1595
|
+
if (!tc?.client?.clientId || !tc.tenantId) {
|
|
1596
|
+
logger.warn("[Callback] OAuth config recovery: tenant resolve missing client or tenantId", {
|
|
1597
|
+
tenantKey
|
|
1598
|
+
});
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
storeTenantConfig(tc);
|
|
1602
|
+
return {
|
|
1603
|
+
clientId: tc.client.clientId.trim() || null,
|
|
1604
|
+
redirectUri: tc.client.redirectUri?.trim() || getRedirectUri(),
|
|
1605
|
+
tenantId: tc.tenantId.trim() || null
|
|
1606
|
+
};
|
|
1607
|
+
} catch (e) {
|
|
1608
|
+
logger.warn("[Callback] OAuth config recovery: tenant re-resolve failed", {
|
|
1609
|
+
tenantKey,
|
|
1610
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1611
|
+
});
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
async function handleCallback() {
|
|
1616
|
+
const runtimeSearch = getAuthRuntime()?.urlProvider.getCurrentUrl().search;
|
|
1617
|
+
const windowSearch = typeof window !== "undefined" ? window.location.search : "";
|
|
1618
|
+
logger.debug("[Callback] handleCallback invoked", {
|
|
1619
|
+
isProcessed: isProcessed(),
|
|
1620
|
+
isProcessing: isProcessing(),
|
|
1621
|
+
isCallbackUrl: isCallbackUrl(),
|
|
1622
|
+
runtimeSearch: runtimeSearch ?? "(none)",
|
|
1623
|
+
windowSearch: windowSearch || "(none)"
|
|
1624
|
+
});
|
|
1625
|
+
if (isProcessed()) {
|
|
1626
|
+
logger.warn("[Callback] Early exit: callback already processed");
|
|
1627
|
+
if (isCallbackUrl()) {
|
|
1628
|
+
if (!isProcessing()) cleanUrlSoft();
|
|
1629
|
+
}
|
|
1630
|
+
clearFlowFlags();
|
|
1631
|
+
return true;
|
|
1632
|
+
}
|
|
1633
|
+
if (isProcessing()) {
|
|
1634
|
+
logger.warn("[Callback] Early exit: callback processing already in progress");
|
|
1635
|
+
return true;
|
|
1636
|
+
}
|
|
1637
|
+
const storedTenantKey = getTenantKey();
|
|
1638
|
+
const currentTenantKey = resolveTenantKey() ?? storedTenantKey;
|
|
1639
|
+
if (currentTenantKey && storedTenantKey && currentTenantKey !== storedTenantKey) {
|
|
1640
|
+
logger.warn("[Callback] Early exit: tenant key changed before exchange", {
|
|
1641
|
+
currentTenantKey,
|
|
1642
|
+
storedTenantKey
|
|
1643
|
+
});
|
|
1644
|
+
clearAllStorage();
|
|
1645
|
+
clearAllAuthFlags();
|
|
1646
|
+
clearProcessing();
|
|
1647
|
+
setStorage(getStorageKey("tenant_switched"), "true");
|
|
1648
|
+
setStorage(getStorageKey("new_tenant_key"), currentTenantKey);
|
|
1649
|
+
setTenantKey(currentTenantKey);
|
|
1650
|
+
if (isCallbackUrl()) cleanUrlSoft();
|
|
1651
|
+
return false;
|
|
1652
|
+
}
|
|
1653
|
+
if (!isCallbackUrl()) {
|
|
1654
|
+
logger.warn("[Callback] Early exit: URL is not callback URL");
|
|
1655
|
+
return false;
|
|
1656
|
+
}
|
|
1657
|
+
markProcessing();
|
|
1658
|
+
clearFlowFlags();
|
|
1659
|
+
urlCleaned = false;
|
|
1660
|
+
try {
|
|
1661
|
+
const search = getAuthRuntime()?.urlProvider.getCurrentUrl().search ?? (typeof window !== "undefined" ? window.location.search : "");
|
|
1662
|
+
const params = new URLSearchParams(search);
|
|
1663
|
+
const code = params.get("code");
|
|
1664
|
+
const state = params.get("state");
|
|
1665
|
+
const error = params.get("error");
|
|
1666
|
+
if (error || !code || !state) {
|
|
1667
|
+
logger.error("[Callback] Early exit: invalid OAuth callback params", { error, code, state });
|
|
1668
|
+
clearAllAuthFlags();
|
|
1669
|
+
clearProcessing();
|
|
1670
|
+
cleanUrlSoft();
|
|
1671
|
+
return false;
|
|
1672
|
+
}
|
|
1673
|
+
const storedState = getState();
|
|
1674
|
+
if (!storedState || storedState !== state) {
|
|
1675
|
+
const currentTenantKeyFromUrl = extractTenantKey();
|
|
1676
|
+
const resolvedTenantKey = resolveTenantKey();
|
|
1677
|
+
const tenantKeyChanged = currentTenantKeyFromUrl && resolvedTenantKey && currentTenantKeyFromUrl !== resolvedTenantKey;
|
|
1678
|
+
if (tenantKeyChanged) {
|
|
1679
|
+
logger.warn("[Callback] Early exit: state mismatch due to tenant key change");
|
|
1680
|
+
} else {
|
|
1681
|
+
logger.error("[Callback] Early exit: OAuth state mismatch - redirecting to login");
|
|
1682
|
+
}
|
|
1683
|
+
clearAllAuthFlags();
|
|
1684
|
+
clearProcessing();
|
|
1685
|
+
cleanUrlSoft();
|
|
1686
|
+
setStorage(getStorageKey("state_mismatch"), "true");
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
const codeVerifier = getCodeVerifier();
|
|
1690
|
+
if (!codeVerifier) {
|
|
1691
|
+
logger.error("[Callback] Early exit: missing PKCE verifier");
|
|
1692
|
+
clearAllAuthFlags();
|
|
1693
|
+
clearProcessing();
|
|
1694
|
+
cleanUrlSoft();
|
|
1695
|
+
return false;
|
|
1696
|
+
}
|
|
1697
|
+
let clientId = getClientId();
|
|
1698
|
+
let redirectUri = getRedirectUri();
|
|
1699
|
+
let tenantId = getTenantId();
|
|
1700
|
+
if (!clientId || !redirectUri || !tenantId) {
|
|
1701
|
+
logger.warn("[Callback] OAuth config incomplete \u2014 attempting tenant re-resolve", {
|
|
1702
|
+
hasClientId: !!clientId,
|
|
1703
|
+
hasRedirectUri: !!redirectUri,
|
|
1704
|
+
hasTenantId: !!tenantId
|
|
1705
|
+
});
|
|
1706
|
+
const recovered = await tryRecoverOAuthConfigFromTenant();
|
|
1707
|
+
if (recovered) {
|
|
1708
|
+
if (!redirectUri) redirectUri = recovered.redirectUri;
|
|
1709
|
+
if (!tenantId) tenantId = recovered.tenantId;
|
|
1710
|
+
clientId = getClientId() ?? normalizeClientIdForOAuthTokenBody(recovered.clientId) ?? null;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
if (!clientId || !redirectUri || !tenantId) {
|
|
1714
|
+
logger.error("[Callback] Early exit: missing OAuth config during callback", {
|
|
1715
|
+
hasClientId: !!clientId,
|
|
1716
|
+
hasRedirectUri: !!redirectUri,
|
|
1717
|
+
hasTenantId: !!tenantId
|
|
1718
|
+
});
|
|
1719
|
+
clearAllAuthFlags();
|
|
1720
|
+
clearProcessing();
|
|
1721
|
+
cleanUrlSoft();
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1724
|
+
const existingToken = getAccessToken();
|
|
1725
|
+
if (existingToken && !isAccessTokenExpired()) {
|
|
1726
|
+
logger.warn("[Callback] Early exit: valid token already exists - skipping code exchange");
|
|
1727
|
+
removeStorage(STORAGE_KEYS.codeVerifier);
|
|
1728
|
+
removeStorage(STORAGE_KEYS.state);
|
|
1729
|
+
clearAllAuthFlags();
|
|
1730
|
+
markProcessed();
|
|
1731
|
+
cleanUrlSoft();
|
|
1732
|
+
resetRedirectLoopGuard();
|
|
1733
|
+
emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
|
|
1734
|
+
return true;
|
|
1735
|
+
}
|
|
1736
|
+
logger.info("[Callback] Entering token exchange");
|
|
1737
|
+
const tokenData = await exchangeCodeForTokens({
|
|
1738
|
+
code,
|
|
1739
|
+
codeVerifier,
|
|
1740
|
+
clientId,
|
|
1741
|
+
redirectUri,
|
|
1742
|
+
tenantId
|
|
1743
|
+
});
|
|
1744
|
+
if (!tokenData?.access_token) {
|
|
1745
|
+
logger.error("Token exchange returned no access token");
|
|
1746
|
+
clearProcessing();
|
|
1747
|
+
clearAllAuthFlags();
|
|
1748
|
+
cleanUrlSoft();
|
|
1749
|
+
emitAuthEvent(AuthEventNames.CALLBACK_FAILED, { message: "No access token returned" });
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
1752
|
+
removeStorage(STORAGE_KEYS.codeVerifier);
|
|
1753
|
+
removeStorage(STORAGE_KEYS.state);
|
|
1754
|
+
clearAllAuthFlags();
|
|
1755
|
+
markProcessed();
|
|
1756
|
+
cleanUrlSoft();
|
|
1757
|
+
logger.info("OAuth callback processed successfully");
|
|
1758
|
+
resetRedirectLoopGuard();
|
|
1759
|
+
emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
|
|
1760
|
+
return true;
|
|
1761
|
+
} catch (err) {
|
|
1762
|
+
if (axios.isAxiosError(err)) {
|
|
1763
|
+
const status = err.response?.status;
|
|
1764
|
+
const responseData = err.response?.data;
|
|
1765
|
+
const errorCode = responseData?.error;
|
|
1766
|
+
const errorDescription = responseData?.error_description || responseData?.details || "";
|
|
1767
|
+
if (status === 400 && errorCode === "invalid_grant" && (String(errorDescription).includes("already used") || String(errorDescription).includes("Code not found") || String(errorDescription).includes("expired, already used"))) {
|
|
1768
|
+
const existingToken2 = getAccessToken();
|
|
1769
|
+
const tokenValid2 = existingToken2 && !isAccessTokenExpired();
|
|
1770
|
+
if (tokenValid2) {
|
|
1771
|
+
removeStorage(STORAGE_KEYS.codeVerifier);
|
|
1772
|
+
removeStorage(STORAGE_KEYS.state);
|
|
1773
|
+
clearAllAuthFlags();
|
|
1774
|
+
markProcessed();
|
|
1775
|
+
cleanUrlSoft();
|
|
1776
|
+
logger.info("OAuth callback processed successfully (code already used, but token exists)");
|
|
1777
|
+
resetRedirectLoopGuard();
|
|
1778
|
+
emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
|
|
1779
|
+
return true;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
emitAuthEvent(AuthEventNames.CALLBACK_FAILED, {
|
|
1783
|
+
error: err.message,
|
|
1784
|
+
message: responseData?.error_description ?? err.message
|
|
1785
|
+
});
|
|
1786
|
+
logger.error("OAuth callback failed (Axios error)", {
|
|
1787
|
+
status,
|
|
1788
|
+
statusText: err.response?.statusText,
|
|
1789
|
+
responseData: err.response?.data,
|
|
1790
|
+
message: err.message,
|
|
1791
|
+
url: err.config?.url
|
|
1792
|
+
});
|
|
1793
|
+
} else {
|
|
1794
|
+
logger.error("OAuth callback failed", {
|
|
1795
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
clearAllAuthFlags();
|
|
1799
|
+
const existingToken = getAccessToken();
|
|
1800
|
+
const tokenValid = existingToken && !isAccessTokenExpired();
|
|
1801
|
+
if (tokenValid) {
|
|
1802
|
+
markProcessed();
|
|
1803
|
+
cleanUrlSoft();
|
|
1804
|
+
resetRedirectLoopGuard();
|
|
1805
|
+
emitAuthEvent(AuthEventNames.CALLBACK_SUCCESS);
|
|
1806
|
+
return true;
|
|
1807
|
+
}
|
|
1808
|
+
emitAuthEvent(AuthEventNames.CALLBACK_FAILED, {
|
|
1809
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1810
|
+
});
|
|
1811
|
+
clearProcessing();
|
|
1812
|
+
if (isCallbackUrl()) cleanUrlSoft();
|
|
1813
|
+
return false;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// src/application/flows/CallbackFlow.ts
|
|
1818
|
+
var CallbackFlow = class {
|
|
1819
|
+
async execute(context) {
|
|
1820
|
+
const { runtime } = context;
|
|
1821
|
+
const { urlProvider, logger: log } = runtime;
|
|
1822
|
+
const getUrl = () => urlProvider.getCurrentUrl();
|
|
1823
|
+
clearFlowFlags();
|
|
1824
|
+
const handled = await handleCallback();
|
|
1825
|
+
if (handled) {
|
|
1826
|
+
if (isCallbackUrlFromUrl(getUrl())) {
|
|
1827
|
+
if (isCallbackProcessing()) return { completed: true };
|
|
1828
|
+
const u = getUrl();
|
|
1829
|
+
urlProvider.redirect(`${u.origin}${u.pathname}`);
|
|
1830
|
+
return { completed: true };
|
|
1831
|
+
}
|
|
1832
|
+
return { completed: true };
|
|
1833
|
+
}
|
|
1834
|
+
if (isCallbackUrlFromUrl(getUrl())) {
|
|
1835
|
+
const u = getUrl();
|
|
1836
|
+
urlProvider.redirect(`${u.origin}${u.pathname}`);
|
|
1837
|
+
}
|
|
1838
|
+
return { completed: true };
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1842
|
+
// src/application/flows/AuthenticatedFlow.ts
|
|
1843
|
+
var AuthenticatedFlow = class {
|
|
1844
|
+
async execute(_context) {
|
|
1845
|
+
return { completed: true };
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
|
|
1849
|
+
// src/application/engine/defaultEngine.ts
|
|
1850
|
+
var defaultEngine = null;
|
|
1851
|
+
function getDefaultAuthEngine() {
|
|
1852
|
+
return defaultEngine;
|
|
1853
|
+
}
|
|
1854
|
+
function setDefaultAuthEngine(engine) {
|
|
1855
|
+
defaultEngine = engine;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// src/application/token.ts
|
|
1859
|
+
var lastNoRefreshLog = 0;
|
|
1860
|
+
var NO_REFRESH_LOG_INTERVAL_MS = 5e3;
|
|
1861
|
+
async function ensureValidAccessToken() {
|
|
1862
|
+
const engine = getDefaultAuthEngine();
|
|
1863
|
+
if (engine) {
|
|
1864
|
+
try {
|
|
1865
|
+
return await engine.getAccessToken();
|
|
1866
|
+
} catch {
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
const token = getAccessToken();
|
|
1870
|
+
if (token && !isAccessTokenExpired()) return token;
|
|
1871
|
+
const refreshToken = getRefreshToken();
|
|
1872
|
+
if (!refreshToken) {
|
|
1873
|
+
const now = Date.now();
|
|
1874
|
+
if (now - lastNoRefreshLog >= NO_REFRESH_LOG_INTERVAL_MS) {
|
|
1875
|
+
lastNoRefreshLog = now;
|
|
1876
|
+
logger.debug("No refresh token - cannot refresh; login required");
|
|
1877
|
+
}
|
|
1878
|
+
throw new Error("No refresh token; login required");
|
|
1879
|
+
}
|
|
1880
|
+
return await refreshAccessToken();
|
|
1881
|
+
}
|
|
1882
|
+
function getAccessToken2() {
|
|
1883
|
+
return getAccessToken();
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// src/application/flows/RefreshFlow.ts
|
|
1887
|
+
var RefreshFlow = class {
|
|
1888
|
+
async execute(context) {
|
|
1889
|
+
const { runtime } = context;
|
|
1890
|
+
const { logger: log } = runtime;
|
|
1891
|
+
const getUrl = () => runtime.urlProvider.getCurrentUrl();
|
|
1892
|
+
log.debug("Token expired, attempting to refresh...");
|
|
1893
|
+
try {
|
|
1894
|
+
const refreshedToken = await ensureValidAccessToken();
|
|
1895
|
+
if (refreshedToken) {
|
|
1896
|
+
logger.debug("Token refreshed successfully, bootstrap complete");
|
|
1897
|
+
return { completed: true };
|
|
1898
|
+
}
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
logger.warn("Token refresh failed, will trigger new auth flow", {
|
|
1901
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
const tenantKey = resolveTenantKey();
|
|
1905
|
+
log.debug("[RefreshFlow] Tenant key resolved", {
|
|
1906
|
+
tenantKey: tenantKey ?? "(null)",
|
|
1907
|
+
source: tenantKey ? "resolved" : "none"
|
|
1908
|
+
});
|
|
1909
|
+
if (!tenantKey) {
|
|
1910
|
+
throw new SanitizedError(
|
|
1911
|
+
"TENANT_KEY_RESOLUTION_FAILED",
|
|
1912
|
+
"Unable to resolve tenant",
|
|
1913
|
+
new Error(`Tenant key missing for refresh flow at url=${getUrl().href}`)
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
|
|
1917
|
+
if (!tenantConfig) {
|
|
1918
|
+
throw new SanitizedError(
|
|
1919
|
+
"TENANT_CONFIG_RESOLUTION_FAILED",
|
|
1920
|
+
"Unable to resolve tenant configuration",
|
|
1921
|
+
new Error(`Tenant config resolution failed for tenantKey: ${tenantKey}`)
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
setTenantKey(tenantKey);
|
|
1925
|
+
storeTenantConfig(tenantConfig);
|
|
1926
|
+
await initiateAuthFlow(tenantConfig);
|
|
1927
|
+
return { completed: true };
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
|
|
1931
|
+
// src/application/flows/TenantSwitchFlow.ts
|
|
1932
|
+
var TenantSwitchFlow = class {
|
|
1933
|
+
async execute(context) {
|
|
1934
|
+
const { runtime } = context;
|
|
1935
|
+
const { urlProvider, logger: log } = runtime;
|
|
1936
|
+
const getUrl = () => urlProvider.getCurrentUrl();
|
|
1937
|
+
const newTenantKey = getStorage(getStorageKey("new_tenant_key"));
|
|
1938
|
+
log.info("Tenant switch detected - starting fresh login", { newTenantKey });
|
|
1939
|
+
emitAuthEvent(AuthEventNames.TENANT_SWITCHED, { tenantKey: newTenantKey ?? void 0 });
|
|
1940
|
+
removeStorage(getStorageKey("tenant_switched"));
|
|
1941
|
+
removeStorage(getStorageKey("new_tenant_key"));
|
|
1942
|
+
const tenantKey = (newTenantKey || resolveTenantKey())?.trim() || null;
|
|
1943
|
+
log.debug("[TenantSwitchFlow] Tenant key resolved", {
|
|
1944
|
+
tenantKey: tenantKey ?? "(null)",
|
|
1945
|
+
fromNewTenantKey: newTenantKey ?? "(null)",
|
|
1946
|
+
source: newTenantKey ? "new_tenant_key" : tenantKey ? "resolved" : "none"
|
|
1947
|
+
});
|
|
1948
|
+
if (!tenantKey) {
|
|
1949
|
+
throw new SanitizedError(
|
|
1950
|
+
"TENANT_KEY_RESOLUTION_FAILED",
|
|
1951
|
+
"Unable to resolve tenant after switch",
|
|
1952
|
+
new Error(`Tenant key missing after switch at url=${getUrl().href}`)
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
|
|
1956
|
+
if (!tenantConfig) {
|
|
1957
|
+
throw new SanitizedError(
|
|
1958
|
+
"TENANT_CONFIG_RESOLUTION_FAILED",
|
|
1959
|
+
"Unable to resolve tenant configuration after switch",
|
|
1960
|
+
new Error(`Tenant config resolution failed for tenantKey: ${tenantKey}`)
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
try {
|
|
1964
|
+
const cfg = getAuthConfig2();
|
|
1965
|
+
writeHostConfigCache({
|
|
1966
|
+
apiBaseUrl: cfg.apiBaseUrl,
|
|
1967
|
+
authServerUrl: cfg.authServerUrl,
|
|
1968
|
+
callbackUrl: cfg.callbackUrl,
|
|
1969
|
+
appId: cfg.appId,
|
|
1970
|
+
clientCode: tenantConfig.client?.clientCode ?? cfg.clientCode ?? "",
|
|
1971
|
+
clientId: cfg.clientId || tenantConfig.client?.clientId,
|
|
1972
|
+
tenantId: tenantConfig.tenantId ?? cfg.tenantId,
|
|
1973
|
+
tenantKey,
|
|
1974
|
+
tenantCode: tenantKey
|
|
1975
|
+
});
|
|
1976
|
+
} catch {
|
|
1977
|
+
}
|
|
1978
|
+
setTenantKey(tenantKey);
|
|
1979
|
+
storeTenantConfig(tenantConfig);
|
|
1980
|
+
await initiateAuthFlow(tenantConfig);
|
|
1981
|
+
return { completed: true };
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
|
|
1985
|
+
// src/application/flows/LoginFlow.ts
|
|
1986
|
+
var LoginFlow = class {
|
|
1987
|
+
async execute(context) {
|
|
1988
|
+
const { runtime } = context;
|
|
1989
|
+
const { logger: log } = runtime;
|
|
1990
|
+
const getUrl = () => runtime.urlProvider.getCurrentUrl();
|
|
1991
|
+
const url = getUrl();
|
|
1992
|
+
log.debug("[LoginFlow] Start", { url: url.href, pathname: url.pathname, search: url.search });
|
|
1993
|
+
const callbackProcessed = getStorage(getStorageKey("callback_processed")) === "true";
|
|
1994
|
+
const token = getAccessToken();
|
|
1995
|
+
if (callbackProcessed && !token) {
|
|
1996
|
+
log.warn("Callback processed but token still not available - clearing flag and allowing fresh login");
|
|
1997
|
+
removeStorage(getStorageKey("callback_processed"));
|
|
1998
|
+
clearCallbackProcessing();
|
|
1999
|
+
}
|
|
2000
|
+
const wasStateMismatch = getStorage(getStorageKey("state_mismatch")) === "true";
|
|
2001
|
+
if (wasStateMismatch) removeStorage(getStorageKey("state_mismatch"));
|
|
2002
|
+
const tenantKey = resolveTenantKey();
|
|
2003
|
+
log.debug("[LoginFlow] Tenant key resolved", {
|
|
2004
|
+
tenantKey: tenantKey ?? "(null)",
|
|
2005
|
+
source: tenantKey ? "resolved" : "none"
|
|
2006
|
+
});
|
|
2007
|
+
if (!tenantKey) {
|
|
2008
|
+
throw new SanitizedError(
|
|
2009
|
+
"TENANT_KEY_RESOLUTION_FAILED",
|
|
2010
|
+
"Unable to resolve tenant",
|
|
2011
|
+
new Error(`Tenant key missing for login flow at url=${url.href}`)
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
log.debug("[LoginFlow] Resolving tenant config", { tenantKey });
|
|
2015
|
+
const tenantConfig = await resolveTenantConfigCached(tenantKey, runtime.tenantResolver);
|
|
2016
|
+
log.debug("[LoginFlow] Tenant config resolved", {
|
|
2017
|
+
ok: !!tenantConfig,
|
|
2018
|
+
tenantId: tenantConfig?.tenantId,
|
|
2019
|
+
authServer: tenantConfig?.authServer ? "(set)" : "(empty)",
|
|
2020
|
+
clientId: tenantConfig?.client?.clientId,
|
|
2021
|
+
redirectUri: tenantConfig?.client?.redirectUri
|
|
2022
|
+
});
|
|
2023
|
+
if (!tenantConfig) {
|
|
2024
|
+
throw new SanitizedError(
|
|
2025
|
+
"TENANT_CONFIG_RESOLUTION_FAILED",
|
|
2026
|
+
"Unable to resolve tenant configuration",
|
|
2027
|
+
new Error(`Tenant config resolution failed for tenantKey: ${tenantKey}`)
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
setTenantKey(tenantKey);
|
|
2031
|
+
storeTenantConfig(tenantConfig);
|
|
2032
|
+
log.debug("[LoginFlow] Stored tenant config, initiating auth flow");
|
|
2033
|
+
await initiateAuthFlow(tenantConfig);
|
|
2034
|
+
log.debug("[LoginFlow] Auth flow initiated (redirect should happen)");
|
|
2035
|
+
return { completed: true };
|
|
2036
|
+
}
|
|
2037
|
+
};
|
|
2038
|
+
|
|
2039
|
+
// src/application/flows/getFlowForState.ts
|
|
2040
|
+
var callbackFlow = new CallbackFlow();
|
|
2041
|
+
var authenticatedFlow = new AuthenticatedFlow();
|
|
2042
|
+
var refreshFlow = new RefreshFlow();
|
|
2043
|
+
var tenantSwitchFlow = new TenantSwitchFlow();
|
|
2044
|
+
var loginFlow = new LoginFlow();
|
|
2045
|
+
function getFlowForState(state) {
|
|
2046
|
+
switch (state) {
|
|
2047
|
+
case "PROCESSING_CALLBACK" /* PROCESSING_CALLBACK */:
|
|
2048
|
+
return callbackFlow;
|
|
2049
|
+
case "AUTHENTICATED" /* AUTHENTICATED */:
|
|
2050
|
+
return authenticatedFlow;
|
|
2051
|
+
case "TOKEN_EXPIRED" /* TOKEN_EXPIRED */:
|
|
2052
|
+
return refreshFlow;
|
|
2053
|
+
case "TENANT_SWITCH" /* TENANT_SWITCH */:
|
|
2054
|
+
return tenantSwitchFlow;
|
|
2055
|
+
case "LOGIN_REQUIRED" /* LOGIN_REQUIRED */:
|
|
2056
|
+
case "INITIALIZING" /* INITIALIZING */:
|
|
2057
|
+
case "LOGGED_OUT" /* LOGGED_OUT */:
|
|
2058
|
+
case "ERROR" /* ERROR */:
|
|
2059
|
+
default:
|
|
2060
|
+
return loginFlow;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// src/application/run-bootstrap.ts
|
|
2065
|
+
function createStorageReader() {
|
|
2066
|
+
return (suffix) => getStorage(getStorageKey(suffix));
|
|
2067
|
+
}
|
|
2068
|
+
async function runBootstrap() {
|
|
2069
|
+
const runtime = getAuthRuntime();
|
|
2070
|
+
if (!runtime) {
|
|
2071
|
+
logger.warn(
|
|
2072
|
+
"Auth runtime not set; skipping bootstrap. Set runtime via AuthProvider or setAuthRuntime()."
|
|
2073
|
+
);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
if (isBootstrapInProgress()) {
|
|
2077
|
+
logger.debug("Bootstrap already in progress - skipping duplicate run");
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
setBootstrapInProgress();
|
|
2081
|
+
try {
|
|
2082
|
+
enforceHttps(true);
|
|
2083
|
+
const url = runtime.urlProvider.getCurrentUrl();
|
|
2084
|
+
const token = getAccessToken();
|
|
2085
|
+
const expired = isAccessTokenExpired();
|
|
2086
|
+
const hasValidToken = !!(token && !expired);
|
|
2087
|
+
const hasToken = !!token;
|
|
2088
|
+
const hasRefreshToken = !!getRefreshToken();
|
|
2089
|
+
const state = resolveAuthLifecycleState({
|
|
2090
|
+
url,
|
|
2091
|
+
getStorage: createStorageReader(),
|
|
2092
|
+
isCallbackUrl: isCallbackUrlFromUrl,
|
|
2093
|
+
hasValidToken,
|
|
2094
|
+
hasToken,
|
|
2095
|
+
hasRefreshToken,
|
|
2096
|
+
isTokenExpired: expired
|
|
2097
|
+
});
|
|
2098
|
+
emitAuthEvent(AuthEventNames.BOOTSTRAP_STATE_DETECTED, { state });
|
|
2099
|
+
const flow = getFlowForState(state);
|
|
2100
|
+
const context = { runtime, url, state };
|
|
2101
|
+
await flow.execute(context);
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
if (error instanceof SanitizedError) throw error;
|
|
2104
|
+
const r = getAuthRuntime();
|
|
2105
|
+
(r?.logger ?? logger).error("Bootstrap failed", {
|
|
2106
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2107
|
+
});
|
|
2108
|
+
throw new Error(
|
|
2109
|
+
sanitizeErrorMessage(error, "Authentication initialization failed")
|
|
2110
|
+
);
|
|
2111
|
+
} finally {
|
|
2112
|
+
clearBootstrapInProgress();
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// src/infrastructure/watchers/callbacks.ts
|
|
2117
|
+
var sessionClearedCallback = null;
|
|
2118
|
+
function setSessionClearedCallback(cb) {
|
|
2119
|
+
sessionClearedCallback = cb;
|
|
2120
|
+
}
|
|
2121
|
+
function getSessionClearedCallback() {
|
|
2122
|
+
return sessionClearedCallback;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// src/application/bootstrap.ts
|
|
2126
|
+
async function bootstrapImpl() {
|
|
2127
|
+
const engine = getDefaultAuthEngine();
|
|
2128
|
+
if (engine) {
|
|
2129
|
+
await engine.init();
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
await runBootstrap();
|
|
2133
|
+
}
|
|
2134
|
+
setSessionClearedCallback(() => bootstrapImpl());
|
|
2135
|
+
async function bootstrap() {
|
|
2136
|
+
return bootstrapImpl();
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
// src/infrastructure/watchers/watcher-guard.ts
|
|
2140
|
+
var suspended = false;
|
|
2141
|
+
var generation = 0;
|
|
2142
|
+
function suspendWatchers() {
|
|
2143
|
+
suspended = true;
|
|
2144
|
+
generation += 1;
|
|
2145
|
+
return generation;
|
|
2146
|
+
}
|
|
2147
|
+
function resumeWatchers(expectedGen) {
|
|
2148
|
+
if (expectedGen !== void 0 && expectedGen !== generation) return;
|
|
2149
|
+
suspended = false;
|
|
2150
|
+
}
|
|
2151
|
+
function areWatchersSuspended() {
|
|
2152
|
+
return suspended;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// src/strategies/cookie/cookie-auth.ts
|
|
2156
|
+
var COOKIE_NAMES = {
|
|
2157
|
+
accessToken: "tenneo_auth_access_token",
|
|
2158
|
+
refreshToken: "tenneo_auth_refresh_token"
|
|
2159
|
+
};
|
|
2160
|
+
|
|
2161
|
+
// src/infrastructure/watchers/cookie-watcher.ts
|
|
2162
|
+
var isWatching = false;
|
|
2163
|
+
var lastCookieSnapshot = "";
|
|
2164
|
+
var lastCookieMap = /* @__PURE__ */ new Map();
|
|
2165
|
+
var watchInterval = null;
|
|
2166
|
+
function getCookieSnapshot() {
|
|
2167
|
+
try {
|
|
2168
|
+
return document.cookie || "";
|
|
2169
|
+
} catch {
|
|
2170
|
+
return "";
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
function getCookieNames() {
|
|
2174
|
+
try {
|
|
2175
|
+
const cookies = document.cookie.split(";");
|
|
2176
|
+
return cookies.map((cookie) => {
|
|
2177
|
+
const eqPos = cookie.indexOf("=");
|
|
2178
|
+
return eqPos > -1 ? cookie.substring(0, eqPos).trim() : cookie.trim();
|
|
2179
|
+
}).filter((name) => name.length > 0);
|
|
2180
|
+
} catch {
|
|
2181
|
+
return [];
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function getCookieMap() {
|
|
2185
|
+
const cookieMap = /* @__PURE__ */ new Map();
|
|
2186
|
+
try {
|
|
2187
|
+
const cookies = document.cookie.split(";");
|
|
2188
|
+
for (const cookie of cookies) {
|
|
2189
|
+
const eqPos = cookie.indexOf("=");
|
|
2190
|
+
if (eqPos > -1) {
|
|
2191
|
+
const name = cookie.substring(0, eqPos).trim();
|
|
2192
|
+
const value = cookie.substring(eqPos + 1).trim();
|
|
2193
|
+
if (name.length > 0) cookieMap.set(name, value);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
} catch {
|
|
2197
|
+
}
|
|
2198
|
+
return cookieMap;
|
|
2199
|
+
}
|
|
2200
|
+
function checkCookiesDeleted() {
|
|
2201
|
+
try {
|
|
2202
|
+
const currentSnapshot = getCookieSnapshot();
|
|
2203
|
+
const currentCookieMap = getCookieMap();
|
|
2204
|
+
const currentCookies = getCookieNames();
|
|
2205
|
+
if (lastCookieSnapshot && !currentSnapshot) {
|
|
2206
|
+
logger.info("All cookies deleted detected");
|
|
2207
|
+
return true;
|
|
2208
|
+
}
|
|
2209
|
+
if (lastCookieMap.size > 0) {
|
|
2210
|
+
const authCookieNames = [
|
|
2211
|
+
COOKIE_NAMES.accessToken,
|
|
2212
|
+
COOKIE_NAMES.refreshToken,
|
|
2213
|
+
"tenneo_auth",
|
|
2214
|
+
"tenneo_auth_access_token",
|
|
2215
|
+
"tenneo_auth_refresh_token",
|
|
2216
|
+
"auth",
|
|
2217
|
+
"session",
|
|
2218
|
+
"token"
|
|
2219
|
+
];
|
|
2220
|
+
const deletedAuthCookies = [];
|
|
2221
|
+
for (const [cookieName] of lastCookieMap) {
|
|
2222
|
+
const isAuthCookie = authCookieNames.some(
|
|
2223
|
+
(authName) => cookieName.toLowerCase().includes(authName.toLowerCase())
|
|
2224
|
+
);
|
|
2225
|
+
if (isAuthCookie && !currentCookieMap.has(cookieName)) {
|
|
2226
|
+
deletedAuthCookies.push(cookieName);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
if (deletedAuthCookies.length > 0) {
|
|
2230
|
+
logger.info("Auth cookies deleted detected", {
|
|
2231
|
+
deletedCookies: deletedAuthCookies,
|
|
2232
|
+
previousCookieCount: lastCookieMap.size,
|
|
2233
|
+
currentCookieCount: currentCookieMap.size
|
|
2234
|
+
});
|
|
2235
|
+
return true;
|
|
2236
|
+
}
|
|
2237
|
+
const hadTenneoAuthAccess = lastCookieMap.has(COOKIE_NAMES.accessToken) || lastCookieMap.has("tenneo_auth_access_token");
|
|
2238
|
+
const hasTenneoAuthAccess = currentCookieMap.has(COOKIE_NAMES.accessToken) || currentCookieMap.has("tenneo_auth_access_token");
|
|
2239
|
+
if (hadTenneoAuthAccess && !hasTenneoAuthAccess) {
|
|
2240
|
+
logger.info("tenneo auth access token cookie deleted detected");
|
|
2241
|
+
return true;
|
|
2242
|
+
}
|
|
2243
|
+
const hadTenneoAuthRefresh = lastCookieMap.has(COOKIE_NAMES.refreshToken) || lastCookieMap.has("tenneo_auth_refresh_token");
|
|
2244
|
+
const hasTenneoAuthRefresh = currentCookieMap.has(COOKIE_NAMES.refreshToken) || currentCookieMap.has("tenneo_auth_refresh_token");
|
|
2245
|
+
if (hadTenneoAuthRefresh && !hasTenneoAuthRefresh) {
|
|
2246
|
+
logger.info("tenneo auth refresh token cookie deleted detected");
|
|
2247
|
+
return true;
|
|
2248
|
+
}
|
|
2249
|
+
if (lastCookieMap.size > currentCookieMap.size && lastCookieMap.size - currentCookieMap.size >= 1) {
|
|
2250
|
+
const deletedCookies = [];
|
|
2251
|
+
for (const [cookieName] of lastCookieMap) {
|
|
2252
|
+
if (!currentCookieMap.has(cookieName)) deletedCookies.push(cookieName);
|
|
2253
|
+
}
|
|
2254
|
+
const deletedAuthCookiesCount = deletedCookies.filter(
|
|
2255
|
+
(name) => authCookieNames.some(
|
|
2256
|
+
(authName) => name.toLowerCase().includes(authName.toLowerCase())
|
|
2257
|
+
)
|
|
2258
|
+
).length;
|
|
2259
|
+
if (deletedAuthCookiesCount > 0) {
|
|
2260
|
+
logger.info("Auth cookies deleted detected (count-based)", {
|
|
2261
|
+
deletedCookies,
|
|
2262
|
+
deletedAuthCount: deletedAuthCookiesCount,
|
|
2263
|
+
previousCount: lastCookieMap.size,
|
|
2264
|
+
currentCount: currentCookieMap.size
|
|
2265
|
+
});
|
|
2266
|
+
return true;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
lastCookieSnapshot = currentSnapshot;
|
|
2271
|
+
lastCookieMap = new Map(currentCookieMap);
|
|
2272
|
+
return false;
|
|
2273
|
+
} catch (error) {
|
|
2274
|
+
logger.warn("Error checking cookies", {
|
|
2275
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2276
|
+
});
|
|
2277
|
+
return false;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
function handleCookieDeletion() {
|
|
2281
|
+
if (areWatchersSuspended()) {
|
|
2282
|
+
logger.debug("Cookie deletion handler skipped \u2014 watchers suspended (logout/teardown)");
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
try {
|
|
2286
|
+
logger.info("Cookies deleted detected - clearing session and redirecting to login");
|
|
2287
|
+
clearAuthStorage();
|
|
2288
|
+
clearAuthSessionStorage();
|
|
2289
|
+
const cb = getSessionClearedCallback();
|
|
2290
|
+
if (cb) {
|
|
2291
|
+
Promise.resolve(cb()).catch((error) => {
|
|
2292
|
+
logger.error("Failed to redirect to login after cookie deletion", {
|
|
2293
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2294
|
+
});
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
} catch (error) {
|
|
2298
|
+
logger.error("Error handling cookie deletion", {
|
|
2299
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
function startCookieWatcher() {
|
|
2304
|
+
if (isWatching) {
|
|
2305
|
+
logger.debug("Cookie watcher already running");
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
isWatching = true;
|
|
2309
|
+
lastCookieSnapshot = getCookieSnapshot();
|
|
2310
|
+
lastCookieMap = getCookieMap();
|
|
2311
|
+
const initialCookies = getCookieNames();
|
|
2312
|
+
const authCookies = initialCookies.filter(
|
|
2313
|
+
(name) => name.toLowerCase().includes("tenneo_auth") || name.toLowerCase().includes("auth") || name.toLowerCase().includes("token")
|
|
2314
|
+
);
|
|
2315
|
+
logger.debug("Cookie watcher started", {
|
|
2316
|
+
initialCookieCount: initialCookies.length,
|
|
2317
|
+
authCookieCount: authCookies.length,
|
|
2318
|
+
authCookies,
|
|
2319
|
+
allCookies: initialCookies
|
|
2320
|
+
});
|
|
2321
|
+
watchInterval = setInterval(async () => {
|
|
2322
|
+
if (areWatchersSuspended()) return;
|
|
2323
|
+
const cookiesDeleted = checkCookiesDeleted();
|
|
2324
|
+
if (cookiesDeleted) {
|
|
2325
|
+
logger.info("Cookies deleted detected by cookie watcher");
|
|
2326
|
+
if (watchInterval) {
|
|
2327
|
+
clearInterval(watchInterval);
|
|
2328
|
+
watchInterval = null;
|
|
2329
|
+
}
|
|
2330
|
+
isWatching = false;
|
|
2331
|
+
try {
|
|
2332
|
+
await handleCookieDeletion();
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
logger.error("Error handling cookie deletion, will restart watcher", {
|
|
2335
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2336
|
+
});
|
|
2337
|
+
} finally {
|
|
2338
|
+
queueMicrotask(() => {
|
|
2339
|
+
if (!isWatching) startCookieWatcher();
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
}, 1e3);
|
|
2344
|
+
logger.info("Cookie watcher started - monitoring for cookie deletion");
|
|
2345
|
+
}
|
|
2346
|
+
function stopCookieWatcher() {
|
|
2347
|
+
isWatching = false;
|
|
2348
|
+
if (watchInterval) {
|
|
2349
|
+
clearInterval(watchInterval);
|
|
2350
|
+
watchInterval = null;
|
|
2351
|
+
}
|
|
2352
|
+
lastCookieSnapshot = "";
|
|
2353
|
+
logger.debug("Cookie watcher stopped");
|
|
2354
|
+
}
|
|
2355
|
+
function resetCookieWatcher() {
|
|
2356
|
+
lastCookieSnapshot = getCookieSnapshot();
|
|
2357
|
+
lastCookieMap = getCookieMap();
|
|
2358
|
+
const currentCookies = getCookieNames();
|
|
2359
|
+
const authCookies = currentCookies.filter(
|
|
2360
|
+
(name) => name.toLowerCase().includes("tenneo_auth") || name.toLowerCase().includes("auth") || name.toLowerCase().includes("token")
|
|
2361
|
+
);
|
|
2362
|
+
logger.debug("Cookie watcher reset", {
|
|
2363
|
+
currentCookieCount: currentCookies.length,
|
|
2364
|
+
authCookieCount: authCookies.length,
|
|
2365
|
+
authCookies
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// src/infrastructure/watchers/storage-watcher.ts
|
|
2370
|
+
var isWatching2 = false;
|
|
2371
|
+
var lastStorageCheck = null;
|
|
2372
|
+
var storageEventListener = null;
|
|
2373
|
+
function checkStorageCleared() {
|
|
2374
|
+
try {
|
|
2375
|
+
const hasAnyAuthData = Object.values(STORAGE_KEYS).some(
|
|
2376
|
+
(key) => getStorage(key) !== null
|
|
2377
|
+
);
|
|
2378
|
+
if (lastStorageCheck === "had_data" && !hasAnyAuthData) return true;
|
|
2379
|
+
lastStorageCheck = hasAnyAuthData ? "had_data" : "no_data";
|
|
2380
|
+
return false;
|
|
2381
|
+
} catch {
|
|
2382
|
+
return false;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
function handleStorageEvent(event) {
|
|
2386
|
+
if (areWatchersSuspended()) return;
|
|
2387
|
+
if (typeof window !== "undefined" && event.storageArea === window.sessionStorage) {
|
|
2388
|
+
const wasCleared = checkStorageCleared();
|
|
2389
|
+
if (wasCleared) {
|
|
2390
|
+
if (areWatchersSuspended()) return;
|
|
2391
|
+
logger.info("Detected manual sessionStorage clear! Redirecting to login...");
|
|
2392
|
+
const cb = getSessionClearedCallback();
|
|
2393
|
+
if (cb) {
|
|
2394
|
+
Promise.resolve(cb()).catch((error) => {
|
|
2395
|
+
logger.error("Failed to redirect to login after manual clear", {
|
|
2396
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2397
|
+
});
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
function startStorageWatcher() {
|
|
2404
|
+
if (isWatching2) return;
|
|
2405
|
+
isWatching2 = true;
|
|
2406
|
+
checkStorageCleared();
|
|
2407
|
+
storageEventListener = handleStorageEvent;
|
|
2408
|
+
window.addEventListener("storage", storageEventListener);
|
|
2409
|
+
const pollInterval = setInterval(() => {
|
|
2410
|
+
if (areWatchersSuspended()) return;
|
|
2411
|
+
const wasCleared = checkStorageCleared();
|
|
2412
|
+
if (wasCleared) {
|
|
2413
|
+
if (areWatchersSuspended()) return;
|
|
2414
|
+
logger.info("Detected manual sessionStorage clear (polling)! Redirecting to login...");
|
|
2415
|
+
clearInterval(pollInterval);
|
|
2416
|
+
const cb = getSessionClearedCallback();
|
|
2417
|
+
if (cb) {
|
|
2418
|
+
Promise.resolve(cb()).catch((error) => {
|
|
2419
|
+
logger.error("Failed to redirect to login after manual clear", {
|
|
2420
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2421
|
+
});
|
|
2422
|
+
if (!isWatching2) startStorageWatcher();
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
}, 1e3);
|
|
2427
|
+
window.__tenneo_auth_storage_watcher_interval = pollInterval;
|
|
2428
|
+
}
|
|
2429
|
+
function stopStorageWatcher() {
|
|
2430
|
+
isWatching2 = false;
|
|
2431
|
+
if (storageEventListener) {
|
|
2432
|
+
window.removeEventListener("storage", storageEventListener);
|
|
2433
|
+
storageEventListener = null;
|
|
2434
|
+
}
|
|
2435
|
+
const win = window;
|
|
2436
|
+
const intervalId = win.__tenneo_auth_storage_watcher_interval;
|
|
2437
|
+
if (intervalId) {
|
|
2438
|
+
clearInterval(intervalId);
|
|
2439
|
+
delete win.__tenneo_auth_storage_watcher_interval;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// src/utils/cookies.ts
|
|
2444
|
+
function clearClientCookies() {
|
|
2445
|
+
try {
|
|
2446
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
2447
|
+
const cookies = document.cookie.split(";");
|
|
2448
|
+
const hostname = window.location.hostname;
|
|
2449
|
+
const hostParts = hostname.split(".").filter(Boolean);
|
|
2450
|
+
const domainsToTry = [hostname];
|
|
2451
|
+
if (hostParts.length > 1) {
|
|
2452
|
+
for (let i = hostParts.length - 2; i >= 0; i--) {
|
|
2453
|
+
const parent = "." + hostParts.slice(i).join(".");
|
|
2454
|
+
if (!domainsToTry.includes(parent)) domainsToTry.push(parent);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
const pathsToTry = ["/", window.location.pathname || "/"].filter(Boolean);
|
|
2458
|
+
for (const cookie of cookies) {
|
|
2459
|
+
const eqPos = cookie.indexOf("=");
|
|
2460
|
+
const name = eqPos > -1 ? cookie.slice(0, eqPos).trim() : cookie.trim();
|
|
2461
|
+
if (!name) continue;
|
|
2462
|
+
for (const path of pathsToTry) {
|
|
2463
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${path}`;
|
|
2464
|
+
for (const domain of domainsToTry) {
|
|
2465
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${path};domain=${domain}`;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
logger.debug("Client-side cookies cleared (best-effort)");
|
|
2470
|
+
} catch (error) {
|
|
2471
|
+
logger.warn("Failed to clear client cookies", {
|
|
2472
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// src/application/logout.ts
|
|
2478
|
+
function buildUrl(baseUrl, path) {
|
|
2479
|
+
const url = new URL(baseUrl);
|
|
2480
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
2481
|
+
url.pathname = url.pathname.endsWith("/") ? `${url.pathname}${normalizedPath.slice(1)}` : `${url.pathname}${normalizedPath}`;
|
|
2482
|
+
return url.toString();
|
|
2483
|
+
}
|
|
2484
|
+
function getCsrfToken() {
|
|
2485
|
+
try {
|
|
2486
|
+
if (typeof document === "undefined") return null;
|
|
2487
|
+
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
2488
|
+
if (meta?.content) return meta.content;
|
|
2489
|
+
const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/i);
|
|
2490
|
+
if (match) return decodeURIComponent(match[1]);
|
|
2491
|
+
return null;
|
|
2492
|
+
} catch {
|
|
2493
|
+
return null;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
async function callServerLogout(logoutUrl) {
|
|
2497
|
+
const csrfToken = getCsrfToken();
|
|
2498
|
+
const headers = {
|
|
2499
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
2500
|
+
Accept: "application/json, */*",
|
|
2501
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
2502
|
+
};
|
|
2503
|
+
if (csrfToken) {
|
|
2504
|
+
headers["X-CSRF-Token"] = csrfToken;
|
|
2505
|
+
}
|
|
2506
|
+
try {
|
|
2507
|
+
const response = await fetch(logoutUrl, {
|
|
2508
|
+
method: "POST",
|
|
2509
|
+
mode: "cors",
|
|
2510
|
+
headers,
|
|
2511
|
+
credentials: "include",
|
|
2512
|
+
body: ""
|
|
2513
|
+
});
|
|
2514
|
+
if (!response.ok) {
|
|
2515
|
+
logger.warn("[logout] Server logout HTTP error", {
|
|
2516
|
+
status: response.status,
|
|
2517
|
+
statusText: response.statusText
|
|
2518
|
+
});
|
|
2519
|
+
return { success: false };
|
|
2520
|
+
}
|
|
2521
|
+
const contentType = response.headers.get("content-type") || "";
|
|
2522
|
+
if (!contentType.includes("application/json")) {
|
|
2523
|
+
logger.info("[logout] Server logout: OK without JSON body", {
|
|
2524
|
+
status: response.status
|
|
2525
|
+
});
|
|
2526
|
+
return { success: true, response: { success: true } };
|
|
2527
|
+
}
|
|
2528
|
+
const text = await response.text();
|
|
2529
|
+
let data = {};
|
|
2530
|
+
if (text?.trim()) {
|
|
2531
|
+
try {
|
|
2532
|
+
data = JSON.parse(text);
|
|
2533
|
+
} catch {
|
|
2534
|
+
logger.warn("[logout] Server returned non-JSON body with JSON content-type");
|
|
2535
|
+
return { success: false };
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
logger.info("[logout] Server logout response", { response: data });
|
|
2539
|
+
if (data && (data.success === true || data.logout === true)) {
|
|
2540
|
+
return { success: true, response: data };
|
|
2541
|
+
}
|
|
2542
|
+
if (!text || text.trim() === "") {
|
|
2543
|
+
return { success: true, response: { success: true } };
|
|
2544
|
+
}
|
|
2545
|
+
logger.warn("[logout] Server logout: unexpected JSON payload", { response: data });
|
|
2546
|
+
return { success: false, response: data };
|
|
2547
|
+
} catch (error) {
|
|
2548
|
+
logger.warn("[logout] Server logout request failed", {
|
|
2549
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2550
|
+
});
|
|
2551
|
+
return { success: false };
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
function performLocalLogoutCleanup() {
|
|
2555
|
+
logger.debug("[logout] Local cleanup: cookies + auth storage");
|
|
2556
|
+
try {
|
|
2557
|
+
getAuthRuntime()?.clearCookies?.clearCookies();
|
|
2558
|
+
} catch (error) {
|
|
2559
|
+
logger.debug("[logout] Runtime cookie clearer failed (ignored)", {
|
|
2560
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
clearClientCookies();
|
|
2564
|
+
try {
|
|
2565
|
+
clearAuthStorage();
|
|
2566
|
+
} catch (error) {
|
|
2567
|
+
logger.warn("[logout] clearAuthStorage failed", {
|
|
2568
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
logger.info("[logout] Local cleanup completed");
|
|
2572
|
+
}
|
|
2573
|
+
function acquireLogoutLock() {
|
|
2574
|
+
const LOCK_KEY = getStorageKey("logout_lock");
|
|
2575
|
+
const now = Date.now();
|
|
2576
|
+
const existingLock = getStorage(LOCK_KEY);
|
|
2577
|
+
if (existingLock) {
|
|
2578
|
+
const lockTime = Number(existingLock);
|
|
2579
|
+
if (!Number.isNaN(lockTime) && now - lockTime < 5e3) {
|
|
2580
|
+
logger.debug("[logout] Lock active \u2014 duplicate tab logout skipped", {
|
|
2581
|
+
now,
|
|
2582
|
+
lockTime
|
|
2583
|
+
});
|
|
2584
|
+
return false;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
setStorage(LOCK_KEY, String(now));
|
|
2588
|
+
return true;
|
|
2589
|
+
}
|
|
2590
|
+
function releaseLogoutLock() {
|
|
2591
|
+
try {
|
|
2592
|
+
removeStorage(getStorageKey("logout_lock"));
|
|
2593
|
+
} catch {
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
var logoutInFlight = null;
|
|
2597
|
+
async function runLogoutPipeline(tenantKey) {
|
|
2598
|
+
const gen = suspendWatchers();
|
|
2599
|
+
logger.info("[logout] Lifecycle: suspend watchers", { generation: gen });
|
|
2600
|
+
stopStorageWatcher();
|
|
2601
|
+
stopCookieWatcher();
|
|
2602
|
+
if (!acquireLogoutLock()) {
|
|
2603
|
+
resumeWatchers(gen);
|
|
2604
|
+
logger.debug("[logout] Lifecycle: resume (skipped \u2014 lock)");
|
|
2605
|
+
return { success: true, message: "Logout already in progress in another tab." };
|
|
2606
|
+
}
|
|
2607
|
+
let serverLogoutSuccess = false;
|
|
2608
|
+
let serverResponse;
|
|
2609
|
+
try {
|
|
2610
|
+
if (tenantKey?.trim()) {
|
|
2611
|
+
applyRuntimeTenantSwitch(tenantKey.trim(), { syncStorageCache: true });
|
|
2612
|
+
logger.info("[logout] Runtime tenant applied for logout context", {
|
|
2613
|
+
tenantKey: tenantKey.trim()
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
const authConfig = getAuthConfig2();
|
|
2617
|
+
const apiAuthBaseUrl = authConfig.authServerUrl;
|
|
2618
|
+
if (apiAuthBaseUrl) {
|
|
2619
|
+
const logoutUrl = buildUrl(apiAuthBaseUrl, "account/logout");
|
|
2620
|
+
logger.info("[logout] Calling auth server logout (POST)", { logoutUrl });
|
|
2621
|
+
const logoutResult = await callServerLogout(logoutUrl);
|
|
2622
|
+
serverLogoutSuccess = logoutResult.success;
|
|
2623
|
+
serverResponse = logoutResult.response;
|
|
2624
|
+
if (!serverLogoutSuccess) {
|
|
2625
|
+
logger.warn("[logout] Server logout did not confirm success \u2014 clearing local session anyway", {
|
|
2626
|
+
logoutUrl,
|
|
2627
|
+
serverResponse
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
} else {
|
|
2631
|
+
logger.warn("[logout] authServerUrl missing \u2014 local logout only");
|
|
2632
|
+
}
|
|
2633
|
+
} catch (error) {
|
|
2634
|
+
logger.error("[logout] Unexpected error before local cleanup", {
|
|
2635
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
try {
|
|
2639
|
+
performLocalLogoutCleanup();
|
|
2640
|
+
} catch (error) {
|
|
2641
|
+
logger.error("[logout] Local cleanup threw", {
|
|
2642
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
try {
|
|
2646
|
+
syncHostConfigCacheFromMemory();
|
|
2647
|
+
logger.debug("[logout] host_config cache refreshed from runtime (not restored from snapshot)");
|
|
2648
|
+
} catch (error) {
|
|
2649
|
+
logger.warn("[logout] Failed to sync host config cache", {
|
|
2650
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
try {
|
|
2654
|
+
resetCookieWatcher();
|
|
2655
|
+
startStorageWatcher();
|
|
2656
|
+
logger.info("[logout] Watchers restarted");
|
|
2657
|
+
} catch (error) {
|
|
2658
|
+
logger.warn("[logout] Watcher restart failed", {
|
|
2659
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
releaseLogoutLock();
|
|
2663
|
+
resumeWatchers(gen);
|
|
2664
|
+
logger.info("[logout] Lifecycle: complete", { serverLogoutSuccess });
|
|
2665
|
+
emitAuthEvent(AuthEventNames.LOGOUT_COMPLETED, {
|
|
2666
|
+
serverLogoutSuccess,
|
|
2667
|
+
tenantKey: tenantKey?.trim() || getAuthConfig2().tenantCode || void 0,
|
|
2668
|
+
message: serverLogoutSuccess ? "Server logout success" : "Local logout (server may be pending)"
|
|
2669
|
+
});
|
|
2670
|
+
return {
|
|
2671
|
+
success: true,
|
|
2672
|
+
message: serverLogoutSuccess ? serverResponse?.message || "Logout successful. Fresh login required." : "Logged out locally. Fresh login required (server logout may have failed).",
|
|
2673
|
+
serverResponse
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
async function logout(tenantKey) {
|
|
2677
|
+
if (logoutInFlight) {
|
|
2678
|
+
logger.debug("[logout] Coalescing with in-flight logout");
|
|
2679
|
+
return logoutInFlight;
|
|
2680
|
+
}
|
|
2681
|
+
logoutInFlight = runLogoutPipeline(tenantKey).finally(() => {
|
|
2682
|
+
logoutInFlight = null;
|
|
2683
|
+
});
|
|
2684
|
+
return logoutInFlight;
|
|
2685
|
+
}
|
|
2686
|
+
var AuthContext = createContext(void 0);
|
|
2687
|
+
function useAuthContext() {
|
|
2688
|
+
const context = useContext(AuthContext);
|
|
2689
|
+
if (!context) {
|
|
2690
|
+
throw new Error("useAuthContext must be used within AuthProvider");
|
|
2691
|
+
}
|
|
2692
|
+
return context;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
// src/infrastructure/tenant/tenant.api.ts
|
|
2696
|
+
var ongoingRequests = /* @__PURE__ */ new Map();
|
|
2697
|
+
var DEFAULT_SCOPES2 = OAUTH_CONSTANTS.SCOPES;
|
|
2698
|
+
function getResolvedTenantBaseUrl() {
|
|
2699
|
+
let base = Config.getApiBaseUrl();
|
|
2700
|
+
if (typeof window !== "undefined" && (!base || base.startsWith("/"))) {
|
|
2701
|
+
base = window.location.origin + (base || "");
|
|
2702
|
+
}
|
|
2703
|
+
return base;
|
|
2704
|
+
}
|
|
2705
|
+
var PUBLIC_CODE_PREFIX = "public-";
|
|
2706
|
+
function getTenantByCodeUrl(tenantCode) {
|
|
2707
|
+
const apiTenantResolutionBaseUrl = getResolvedTenantBaseUrl();
|
|
2708
|
+
const base = (apiTenantResolutionBaseUrl || "").replace(/\/api(?:\/v1)?\/?$/, "") || apiTenantResolutionBaseUrl;
|
|
2709
|
+
return `${base}/api/v1/tenant/public/tenants/by-code/${encodeURIComponent(tenantCode)}`;
|
|
2710
|
+
}
|
|
2711
|
+
async function resolveTenant(tenantKey) {
|
|
2712
|
+
if (!tenantKey || tenantKey.trim() === "") {
|
|
2713
|
+
logger.debug("tenantKey is empty or invalid, skipping API call");
|
|
2714
|
+
return null;
|
|
2715
|
+
}
|
|
2716
|
+
const tenantCode = tenantKey.trim();
|
|
2717
|
+
const requestKey = `resolveTenant:${tenantCode}`;
|
|
2718
|
+
if (ongoingRequests.has(requestKey)) {
|
|
2719
|
+
logger.debug("Tenant resolution reused (in-flight)", { tenantCode });
|
|
2720
|
+
return ongoingRequests.get(requestKey);
|
|
2721
|
+
}
|
|
2722
|
+
logger.info("Tenant resolution API call", { tenantCode });
|
|
2723
|
+
const requestPromise = (async () => {
|
|
2724
|
+
try {
|
|
2725
|
+
const url = getTenantByCodeUrl(tenantCode);
|
|
2726
|
+
logger.debug("GET tenant by-code", { url });
|
|
2727
|
+
let data;
|
|
2728
|
+
try {
|
|
2729
|
+
const response = await fetch(url, { method: "GET" });
|
|
2730
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
2731
|
+
data = await response.json();
|
|
2732
|
+
logger.debug("API response received", { tenantCode });
|
|
2733
|
+
} catch (error) {
|
|
2734
|
+
logger.warn("API call failed", { tenantCode, error });
|
|
2735
|
+
return null;
|
|
2736
|
+
}
|
|
2737
|
+
if (!data || typeof data !== "object") return null;
|
|
2738
|
+
const res = data;
|
|
2739
|
+
if (!res.success || !res.data) return null;
|
|
2740
|
+
const tenant = res.data;
|
|
2741
|
+
if (!tenant.id || !tenant.clientCode) return null;
|
|
2742
|
+
const authServer = Config.getAuthServerUrl();
|
|
2743
|
+
const redirectUri = Config.getRedirectUri();
|
|
2744
|
+
if (!authServer || !redirectUri) {
|
|
2745
|
+
return null;
|
|
2746
|
+
}
|
|
2747
|
+
const clientCodeWithPrefix = tenant.clientCode.startsWith(PUBLIC_CODE_PREFIX) ? tenant.clientCode : `${PUBLIC_CODE_PREFIX}${tenant.clientCode}`;
|
|
2748
|
+
const config = {
|
|
2749
|
+
tenantId: tenant.id,
|
|
2750
|
+
authServer,
|
|
2751
|
+
tenantKey: tenantCode,
|
|
2752
|
+
client: {
|
|
2753
|
+
clientId: tenant.clientCode,
|
|
2754
|
+
clientCode: clientCodeWithPrefix,
|
|
2755
|
+
redirectUri,
|
|
2756
|
+
scopes: DEFAULT_SCOPES2
|
|
2757
|
+
}
|
|
2758
|
+
};
|
|
2759
|
+
logger.debug("Resolved tenant config", { tenantCode });
|
|
2760
|
+
return config;
|
|
2761
|
+
} catch (error) {
|
|
2762
|
+
logger.warn("Unexpected error resolving tenant", { tenantCode, error });
|
|
2763
|
+
return null;
|
|
2764
|
+
} finally {
|
|
2765
|
+
ongoingRequests.delete(requestKey);
|
|
2766
|
+
}
|
|
2767
|
+
})();
|
|
2768
|
+
ongoingRequests.set(requestKey, requestPromise);
|
|
2769
|
+
return requestPromise;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// src/browser/adapters.ts
|
|
2773
|
+
function createConsoleLogger() {
|
|
2774
|
+
return {
|
|
2775
|
+
info(message, meta) {
|
|
2776
|
+
if (typeof console !== "undefined") console.info(message, meta ?? "");
|
|
2777
|
+
},
|
|
2778
|
+
warn(message, meta) {
|
|
2779
|
+
if (typeof console !== "undefined") console.warn(message, meta ?? "");
|
|
2780
|
+
},
|
|
2781
|
+
error(message, meta) {
|
|
2782
|
+
if (typeof console !== "undefined") console.error(message, meta ?? "");
|
|
2783
|
+
},
|
|
2784
|
+
debug(message, meta) {
|
|
2785
|
+
if (typeof console !== "undefined") console.debug(message, meta ?? "");
|
|
2786
|
+
}
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
function createBrowserUrlProvider() {
|
|
2790
|
+
return {
|
|
2791
|
+
getCurrentUrl() {
|
|
2792
|
+
if (typeof window === "undefined") {
|
|
2793
|
+
return { href: "", origin: "", pathname: "/", search: "", hostname: "" };
|
|
2794
|
+
}
|
|
2795
|
+
const w = window;
|
|
2796
|
+
const loc = w.location;
|
|
2797
|
+
return {
|
|
2798
|
+
href: loc.href,
|
|
2799
|
+
origin: loc.origin,
|
|
2800
|
+
pathname: loc.pathname,
|
|
2801
|
+
search: loc.search,
|
|
2802
|
+
hostname: loc.hostname
|
|
2803
|
+
};
|
|
2804
|
+
},
|
|
2805
|
+
redirect(url) {
|
|
2806
|
+
if (typeof window !== "undefined") window.location.replace(url);
|
|
2807
|
+
},
|
|
2808
|
+
replaceState(url) {
|
|
2809
|
+
if (typeof window === "undefined") return;
|
|
2810
|
+
try {
|
|
2811
|
+
const title = typeof document !== "undefined" ? document.title : "";
|
|
2812
|
+
window.history.replaceState({}, title, url);
|
|
2813
|
+
} catch {
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
function createConfigProvider() {
|
|
2819
|
+
return {
|
|
2820
|
+
getApiBaseUrl: () => Config.getApiBaseUrl(),
|
|
2821
|
+
getAuthServerUrl: () => Config.getAuthServerUrl(),
|
|
2822
|
+
getRedirectUri: () => Config.getRedirectUri(),
|
|
2823
|
+
getClientCode: () => Config.getClientCode()
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
function createTenantResolver() {
|
|
2827
|
+
return { resolve: (tenantKey) => resolveTenant(tenantKey) };
|
|
2828
|
+
}
|
|
2829
|
+
function createBrowserCookieClearer() {
|
|
2830
|
+
return {
|
|
2831
|
+
clearCookies() {
|
|
2832
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
2833
|
+
try {
|
|
2834
|
+
const cookies = document.cookie.split(";");
|
|
2835
|
+
cookies.forEach((cookie) => {
|
|
2836
|
+
const eqPos = cookie.indexOf("=");
|
|
2837
|
+
const name = eqPos > -1 ? cookie.substring(0, eqPos).trim() : cookie.trim();
|
|
2838
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/`;
|
|
2839
|
+
const domain = window.location.hostname;
|
|
2840
|
+
const parts = domain.split(".");
|
|
2841
|
+
if (parts.length > 1) {
|
|
2842
|
+
const parentDomain = "." + parts.slice(-2).join(".");
|
|
2843
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${parentDomain}`;
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
} catch {
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
// src/strategies/oauth2-pkce/OAuth2PkceStrategy.ts
|
|
2853
|
+
function createOAuth2PkceStrategy() {
|
|
2854
|
+
return {
|
|
2855
|
+
async initiate(tenantConfig) {
|
|
2856
|
+
storeTenantConfig(tenantConfig);
|
|
2857
|
+
await initiateAuthFlow(tenantConfig);
|
|
2858
|
+
},
|
|
2859
|
+
async handleCallback() {
|
|
2860
|
+
return handleCallback();
|
|
2861
|
+
},
|
|
2862
|
+
async refresh() {
|
|
2863
|
+
return refreshAccessToken();
|
|
2864
|
+
},
|
|
2865
|
+
async logout(options) {
|
|
2866
|
+
await logout(options?.tenantKey);
|
|
2867
|
+
}
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// src/application/engine/eventBus.ts
|
|
2872
|
+
function createEventBus() {
|
|
2873
|
+
const listeners2 = /* @__PURE__ */ new Set();
|
|
2874
|
+
return {
|
|
2875
|
+
subscribe(listener) {
|
|
2876
|
+
listeners2.add(listener);
|
|
2877
|
+
return () => listeners2.delete(listener);
|
|
2878
|
+
},
|
|
2879
|
+
emit(state) {
|
|
2880
|
+
listeners2.forEach((fn) => {
|
|
2881
|
+
try {
|
|
2882
|
+
fn(state);
|
|
2883
|
+
} catch {
|
|
2884
|
+
}
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// src/application/engine/createEngine.ts
|
|
2891
|
+
function stateEquals(a, b) {
|
|
2892
|
+
return a.status === b.status && a.accessToken === b.accessToken && a.expiresAt === b.expiresAt && a.error === b.error;
|
|
2893
|
+
}
|
|
2894
|
+
function deriveState(config) {
|
|
2895
|
+
setAuthRuntime(config);
|
|
2896
|
+
const accessToken = getAccessToken();
|
|
2897
|
+
const expiresAt = getExpiresAt();
|
|
2898
|
+
const expired = isAccessTokenExpired();
|
|
2899
|
+
if (accessToken && !expired) {
|
|
2900
|
+
return {
|
|
2901
|
+
status: "authenticated",
|
|
2902
|
+
accessToken,
|
|
2903
|
+
expiresAt,
|
|
2904
|
+
error: null
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
if (accessToken && expired) {
|
|
2908
|
+
return {
|
|
2909
|
+
status: "error",
|
|
2910
|
+
accessToken,
|
|
2911
|
+
expiresAt,
|
|
2912
|
+
error: "Token expired"
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
return {
|
|
2916
|
+
status: "unauthenticated",
|
|
2917
|
+
accessToken: null,
|
|
2918
|
+
expiresAt: null,
|
|
2919
|
+
error: null
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
function createAuthEngine(config) {
|
|
2923
|
+
const frozenConfig = Object.freeze({ ...config });
|
|
2924
|
+
const bus = createEventBus();
|
|
2925
|
+
let lastEmittedState = null;
|
|
2926
|
+
const strategy = frozenConfig.strategy ?? createOAuth2PkceStrategy();
|
|
2927
|
+
const emitState = () => {
|
|
2928
|
+
const next = deriveState(frozenConfig);
|
|
2929
|
+
if (lastEmittedState !== null && stateEquals(lastEmittedState, next)) return;
|
|
2930
|
+
lastEmittedState = next;
|
|
2931
|
+
bus.emit(next);
|
|
2932
|
+
};
|
|
2933
|
+
return {
|
|
2934
|
+
async init() {
|
|
2935
|
+
setAuthRuntime(frozenConfig);
|
|
2936
|
+
frozenConfig.logger.debug("Engine init: running bootstrap");
|
|
2937
|
+
await runBootstrap();
|
|
2938
|
+
emitState();
|
|
2939
|
+
},
|
|
2940
|
+
async login() {
|
|
2941
|
+
setAuthRuntime(frozenConfig);
|
|
2942
|
+
frozenConfig.logger.debug("Engine login: running bootstrap (may redirect)");
|
|
2943
|
+
await runBootstrap();
|
|
2944
|
+
emitState();
|
|
2945
|
+
},
|
|
2946
|
+
async logout(options) {
|
|
2947
|
+
setAuthRuntime(frozenConfig);
|
|
2948
|
+
frozenConfig.logger.debug("Engine logout");
|
|
2949
|
+
await strategy.logout(options);
|
|
2950
|
+
emitState();
|
|
2951
|
+
},
|
|
2952
|
+
async refresh() {
|
|
2953
|
+
setAuthRuntime(frozenConfig);
|
|
2954
|
+
frozenConfig.logger.debug("Engine refresh");
|
|
2955
|
+
try {
|
|
2956
|
+
const token = await strategy.refresh();
|
|
2957
|
+
emitState();
|
|
2958
|
+
return token;
|
|
2959
|
+
} catch (e) {
|
|
2960
|
+
emitState();
|
|
2961
|
+
throw e;
|
|
2962
|
+
}
|
|
2963
|
+
},
|
|
2964
|
+
async getAccessToken() {
|
|
2965
|
+
setAuthRuntime(frozenConfig);
|
|
2966
|
+
return ensureValidAccessToken();
|
|
2967
|
+
},
|
|
2968
|
+
async handleCallback() {
|
|
2969
|
+
setAuthRuntime(frozenConfig);
|
|
2970
|
+
frozenConfig.logger.debug("Engine handleCallback");
|
|
2971
|
+
const handled = await strategy.handleCallback();
|
|
2972
|
+
emitState();
|
|
2973
|
+
return handled;
|
|
2974
|
+
},
|
|
2975
|
+
subscribe(listener) {
|
|
2976
|
+
return bus.subscribe(listener);
|
|
2977
|
+
},
|
|
2978
|
+
getState() {
|
|
2979
|
+
return deriveState(frozenConfig);
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
function AuthProvider({
|
|
2984
|
+
children,
|
|
2985
|
+
autoInit = true,
|
|
2986
|
+
config,
|
|
2987
|
+
tenantCode,
|
|
2988
|
+
apiBaseUrl,
|
|
2989
|
+
authServerUrl,
|
|
2990
|
+
callbackUrl,
|
|
2991
|
+
appId = "",
|
|
2992
|
+
storageAdapter
|
|
2993
|
+
}) {
|
|
2994
|
+
const mergedTenantCode = tenantCode ?? config?.tenantCode;
|
|
2995
|
+
const mergedApiBaseUrl = apiBaseUrl ?? config?.apiBaseUrl;
|
|
2996
|
+
const mergedAuthServerUrl = authServerUrl ?? config?.authServerUrl;
|
|
2997
|
+
const mergedCallbackUrl = callbackUrl ?? config?.callbackUrl;
|
|
2998
|
+
const resolvedTenantKey = mergedTenantCode ?? Config.getClientCode();
|
|
2999
|
+
const namespace = useMemo(
|
|
3000
|
+
() => ({ tenantId: resolvedTenantKey ?? "", appId: appId ?? "" }),
|
|
3001
|
+
[resolvedTenantKey, appId]
|
|
3002
|
+
);
|
|
3003
|
+
const initRanRef = useRef(false);
|
|
3004
|
+
const engineRef = useRef(null);
|
|
3005
|
+
useLayoutEffect(() => {
|
|
3006
|
+
logger.debug("[AuthProvider] Host config props", {
|
|
3007
|
+
tenantCode: mergedTenantCode ?? "(not provided)",
|
|
3008
|
+
apiBaseUrl: mergedApiBaseUrl ? mergedApiBaseUrl : "(empty)",
|
|
3009
|
+
authServerUrl: mergedAuthServerUrl ? mergedAuthServerUrl : "(empty)",
|
|
3010
|
+
callbackUrl: mergedCallbackUrl ? mergedCallbackUrl : "(empty)"
|
|
3011
|
+
});
|
|
3012
|
+
if (!mergedAuthServerUrl || !mergedAuthServerUrl.trim()) {
|
|
3013
|
+
logger.warn("[AuthProvider] authServerUrl missing from host props");
|
|
3014
|
+
}
|
|
3015
|
+
if (!mergedCallbackUrl || !mergedCallbackUrl.trim()) {
|
|
3016
|
+
logger.warn("[AuthProvider] callbackUrl missing from host props");
|
|
3017
|
+
}
|
|
3018
|
+
setConfig({
|
|
3019
|
+
...mergedTenantCode ? { tenantCode: mergedTenantCode, tenantKey: mergedTenantCode } : {},
|
|
3020
|
+
...mergedApiBaseUrl ? { apiBaseUrl: mergedApiBaseUrl } : {},
|
|
3021
|
+
...mergedAuthServerUrl ? { authServerUrl: mergedAuthServerUrl } : {},
|
|
3022
|
+
...mergedCallbackUrl ? { callbackUrl: mergedCallbackUrl } : {}
|
|
3023
|
+
});
|
|
3024
|
+
logConfigResolutionDebug("AuthProvider host props applied");
|
|
3025
|
+
return () => {
|
|
3026
|
+
clearConfigOverrides();
|
|
3027
|
+
};
|
|
3028
|
+
}, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl]);
|
|
3029
|
+
useEffect(() => {
|
|
3030
|
+
try {
|
|
3031
|
+
writeHostConfigCache({
|
|
3032
|
+
tenantCode: mergedTenantCode ?? "",
|
|
3033
|
+
tenantKey: mergedTenantCode ?? "",
|
|
3034
|
+
appId: appId ?? "",
|
|
3035
|
+
apiBaseUrl: mergedApiBaseUrl ?? "",
|
|
3036
|
+
authServerUrl: mergedAuthServerUrl ?? "",
|
|
3037
|
+
callbackUrl: mergedCallbackUrl ?? ""
|
|
3038
|
+
});
|
|
3039
|
+
} catch {
|
|
3040
|
+
}
|
|
3041
|
+
}, [mergedTenantCode, mergedApiBaseUrl, mergedAuthServerUrl, mergedCallbackUrl, appId]);
|
|
3042
|
+
useLayoutEffect(() => {
|
|
3043
|
+
setStorageKeyPrefix(resolvedTenantKey ?? "");
|
|
3044
|
+
const adapter = storageAdapter !== void 0 && storageAdapter !== null ? storageAdapter : createSecureSessionStorageAdapter(namespace);
|
|
3045
|
+
setStorageContext(adapter, namespace);
|
|
3046
|
+
const runtime = {
|
|
3047
|
+
storageAdapter: adapter,
|
|
3048
|
+
storageNamespace: namespace,
|
|
3049
|
+
logger: createConsoleLogger(),
|
|
3050
|
+
urlProvider: createBrowserUrlProvider(),
|
|
3051
|
+
configProvider: createConfigProvider(),
|
|
3052
|
+
tenantResolver: createTenantResolver(),
|
|
3053
|
+
clearCookies: createBrowserCookieClearer()
|
|
3054
|
+
};
|
|
3055
|
+
setAuthRuntime(runtime);
|
|
3056
|
+
engineRef.current = createAuthEngine(runtime);
|
|
3057
|
+
setDefaultAuthEngine(engineRef.current);
|
|
3058
|
+
return () => {
|
|
3059
|
+
setDefaultAuthEngine(null);
|
|
3060
|
+
engineRef.current = null;
|
|
3061
|
+
setAuthRuntime(null);
|
|
3062
|
+
setStorageContext(null, { tenantId: "", appId: "" });
|
|
3063
|
+
setStorageKeyPrefix("");
|
|
3064
|
+
};
|
|
3065
|
+
}, [resolvedTenantKey, appId, storageAdapter]);
|
|
3066
|
+
useEffect(() => {
|
|
3067
|
+
const resolvedTenantKey2 = mergedTenantCode ?? Config.getClientCode();
|
|
3068
|
+
logger.debug("[AuthProvider] Props from host app", {
|
|
3069
|
+
tenantCode: mergedTenantCode ?? "(not provided)",
|
|
3070
|
+
appId: appId ?? "",
|
|
3071
|
+
resolvedTenantKey: resolvedTenantKey2
|
|
3072
|
+
});
|
|
3073
|
+
if (resolvedTenantKey2) {
|
|
3074
|
+
const previousTenantKey = getStorage(getStorageKey("tenant_key"));
|
|
3075
|
+
const tenantKeyChanged = previousTenantKey && previousTenantKey !== resolvedTenantKey2;
|
|
3076
|
+
if (tenantKeyChanged) {
|
|
3077
|
+
logger.debug("[AuthProvider] Tenant switch detected", { previous: previousTenantKey, next: resolvedTenantKey2 });
|
|
3078
|
+
if (typeof window !== "undefined") {
|
|
3079
|
+
const currentUrl = window.location.href;
|
|
3080
|
+
const isCallback = currentUrl.includes("code=") || currentUrl.includes("state=");
|
|
3081
|
+
if (isCallback) {
|
|
3082
|
+
const cleanUrl = window.location.origin + window.location.pathname;
|
|
3083
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
removeStorage(getStorageKey("access_token"));
|
|
3087
|
+
removeStorage(getStorageKey("refresh_token"));
|
|
3088
|
+
removeStorage(getStorageKey("expires_at"));
|
|
3089
|
+
removeStorage(getStorageKey("state"));
|
|
3090
|
+
removeStorage(getStorageKey("code_verifier"));
|
|
3091
|
+
removeStorage(getStorageKey("callback_processed"));
|
|
3092
|
+
clearCallbackProcessing();
|
|
3093
|
+
clearFlowFlags();
|
|
3094
|
+
setStorage(getStorageKey("props_changed"), "true");
|
|
3095
|
+
setStorage(getStorageKey("tenant_switched"), "true");
|
|
3096
|
+
setStorage(getStorageKey("new_tenant_key"), resolvedTenantKey2);
|
|
3097
|
+
setState({
|
|
3098
|
+
isAuthenticated: false,
|
|
3099
|
+
accessToken: null,
|
|
3100
|
+
isLoading: true,
|
|
3101
|
+
error: null
|
|
3102
|
+
});
|
|
3103
|
+
} else if (previousTenantKey) {
|
|
3104
|
+
logger.debug("[AuthProvider] TenantKey unchanged", { tenantKey: resolvedTenantKey2 });
|
|
3105
|
+
}
|
|
3106
|
+
try {
|
|
3107
|
+
writeHostConfigCache({
|
|
3108
|
+
tenantKey: resolvedTenantKey2,
|
|
3109
|
+
tenantCode: resolvedTenantKey2
|
|
3110
|
+
});
|
|
3111
|
+
logConfigResolutionDebug("AuthProvider tenant key synced to cache", {
|
|
3112
|
+
tenantCode: resolvedTenantKey2,
|
|
3113
|
+
tenantKey: resolvedTenantKey2
|
|
3114
|
+
});
|
|
3115
|
+
} catch {
|
|
3116
|
+
}
|
|
3117
|
+
setStorage(getStorageKey("tenant_key"), resolvedTenantKey2);
|
|
3118
|
+
} else {
|
|
3119
|
+
logger.debug("[AuthProvider] No tenant code determined");
|
|
3120
|
+
}
|
|
3121
|
+
}, [mergedTenantCode, appId]);
|
|
3122
|
+
const [state, setState] = useState({
|
|
3123
|
+
isAuthenticated: false,
|
|
3124
|
+
accessToken: null,
|
|
3125
|
+
isLoading: autoInit,
|
|
3126
|
+
error: null
|
|
3127
|
+
});
|
|
3128
|
+
const syncAuthState = useCallback(() => {
|
|
3129
|
+
try {
|
|
3130
|
+
const engine = engineRef.current;
|
|
3131
|
+
let authenticated;
|
|
3132
|
+
let accessToken;
|
|
3133
|
+
let error = null;
|
|
3134
|
+
if (engine) {
|
|
3135
|
+
const s = engine.getState();
|
|
3136
|
+
authenticated = s.status === "authenticated" && !!s.accessToken;
|
|
3137
|
+
accessToken = s.accessToken;
|
|
3138
|
+
error = s.error;
|
|
3139
|
+
} else {
|
|
3140
|
+
accessToken = getAccessToken();
|
|
3141
|
+
authenticated = !!accessToken && !isAccessTokenExpired();
|
|
3142
|
+
}
|
|
3143
|
+
setState({
|
|
3144
|
+
isAuthenticated: authenticated,
|
|
3145
|
+
accessToken,
|
|
3146
|
+
isLoading: false,
|
|
3147
|
+
error
|
|
3148
|
+
});
|
|
3149
|
+
if (authenticated) {
|
|
3150
|
+
} else {
|
|
3151
|
+
stopStorageWatcher();
|
|
3152
|
+
stopCookieWatcher();
|
|
3153
|
+
}
|
|
3154
|
+
} catch (error) {
|
|
3155
|
+
stopStorageWatcher();
|
|
3156
|
+
stopCookieWatcher();
|
|
3157
|
+
setState({
|
|
3158
|
+
isAuthenticated: false,
|
|
3159
|
+
accessToken: null,
|
|
3160
|
+
isLoading: false,
|
|
3161
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
}, []);
|
|
3165
|
+
useEffect(() => {
|
|
3166
|
+
if (!autoInit) return;
|
|
3167
|
+
const tenantSwitched = getStorage(getStorageKey("tenant_switched")) === "true";
|
|
3168
|
+
const propsChanged = getStorage(getStorageKey("props_changed")) === "true";
|
|
3169
|
+
if (!tenantSwitched && !propsChanged) return;
|
|
3170
|
+
let cancelled = false;
|
|
3171
|
+
const reinit = async () => {
|
|
3172
|
+
try {
|
|
3173
|
+
await engineRef.current?.init();
|
|
3174
|
+
if (!cancelled) {
|
|
3175
|
+
syncAuthState();
|
|
3176
|
+
}
|
|
3177
|
+
} finally {
|
|
3178
|
+
removeStorage(getStorageKey("props_changed"));
|
|
3179
|
+
}
|
|
3180
|
+
};
|
|
3181
|
+
reinit();
|
|
3182
|
+
return () => {
|
|
3183
|
+
cancelled = true;
|
|
3184
|
+
};
|
|
3185
|
+
}, [autoInit, mergedTenantCode, syncAuthState]);
|
|
3186
|
+
useEffect(() => {
|
|
3187
|
+
if (!autoInit) {
|
|
3188
|
+
setState((prev) => ({ ...prev, isLoading: false }));
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
if (initRanRef.current) return;
|
|
3192
|
+
initRanRef.current = true;
|
|
3193
|
+
let cancelled = false;
|
|
3194
|
+
const init = async () => {
|
|
3195
|
+
try {
|
|
3196
|
+
await engineRef.current?.init();
|
|
3197
|
+
if (!cancelled) syncAuthState();
|
|
3198
|
+
} catch (error) {
|
|
3199
|
+
stopStorageWatcher();
|
|
3200
|
+
stopCookieWatcher();
|
|
3201
|
+
if (!cancelled) {
|
|
3202
|
+
setState({
|
|
3203
|
+
isAuthenticated: false,
|
|
3204
|
+
accessToken: null,
|
|
3205
|
+
isLoading: false,
|
|
3206
|
+
error: error instanceof Error ? error.message : "Authentication initialization failed"
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
};
|
|
3211
|
+
init();
|
|
3212
|
+
return () => {
|
|
3213
|
+
cancelled = true;
|
|
3214
|
+
initRanRef.current = false;
|
|
3215
|
+
stopStorageWatcher();
|
|
3216
|
+
stopCookieWatcher();
|
|
3217
|
+
};
|
|
3218
|
+
}, [autoInit, syncAuthState]);
|
|
3219
|
+
const login = useCallback(async () => {
|
|
3220
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
3221
|
+
await engineRef.current?.login();
|
|
3222
|
+
syncAuthState();
|
|
3223
|
+
}, [syncAuthState]);
|
|
3224
|
+
const logout2 = useCallback(async () => {
|
|
3225
|
+
stopStorageWatcher();
|
|
3226
|
+
stopCookieWatcher();
|
|
3227
|
+
setState((prev) => ({ ...prev, isLoading: true }));
|
|
3228
|
+
try {
|
|
3229
|
+
await engineRef.current?.logout({ tenantKey: mergedTenantCode });
|
|
3230
|
+
resetCookieWatcher();
|
|
3231
|
+
} finally {
|
|
3232
|
+
stopStorageWatcher();
|
|
3233
|
+
stopCookieWatcher();
|
|
3234
|
+
setState({
|
|
3235
|
+
isAuthenticated: false,
|
|
3236
|
+
accessToken: null,
|
|
3237
|
+
isLoading: false,
|
|
3238
|
+
error: null
|
|
3239
|
+
});
|
|
3240
|
+
}
|
|
3241
|
+
}, [mergedTenantCode]);
|
|
3242
|
+
const refreshToken = useCallback(async () => {
|
|
3243
|
+
try {
|
|
3244
|
+
const newToken = await engineRef.current?.refresh() ?? "";
|
|
3245
|
+
syncAuthState();
|
|
3246
|
+
return newToken;
|
|
3247
|
+
} catch (error) {
|
|
3248
|
+
await logout2();
|
|
3249
|
+
throw error;
|
|
3250
|
+
}
|
|
3251
|
+
}, [syncAuthState, logout2]);
|
|
3252
|
+
const getValidAccessToken = useCallback(async () => {
|
|
3253
|
+
try {
|
|
3254
|
+
const token = await engineRef.current?.getAccessToken();
|
|
3255
|
+
if (!token) throw new Error("Not authenticated");
|
|
3256
|
+
syncAuthState();
|
|
3257
|
+
return token;
|
|
3258
|
+
} catch (error) {
|
|
3259
|
+
await logout2();
|
|
3260
|
+
throw error;
|
|
3261
|
+
}
|
|
3262
|
+
}, [syncAuthState, logout2]);
|
|
3263
|
+
const setTokens = useCallback((accessToken) => {
|
|
3264
|
+
setState({
|
|
3265
|
+
isAuthenticated: true,
|
|
3266
|
+
accessToken,
|
|
3267
|
+
isLoading: false,
|
|
3268
|
+
error: null
|
|
3269
|
+
});
|
|
3270
|
+
}, []);
|
|
3271
|
+
const contextValue = useMemo(
|
|
3272
|
+
() => ({
|
|
3273
|
+
...state,
|
|
3274
|
+
login,
|
|
3275
|
+
logout: logout2,
|
|
3276
|
+
refreshToken,
|
|
3277
|
+
getAccessToken: getValidAccessToken,
|
|
3278
|
+
setTokens
|
|
3279
|
+
}),
|
|
3280
|
+
[state, login, logout2, refreshToken, getValidAccessToken, setTokens]
|
|
3281
|
+
);
|
|
3282
|
+
return /* @__PURE__ */ jsx(AuthContext.Provider, { value: contextValue, children });
|
|
3283
|
+
}
|
|
3284
|
+
var PLUGIN_DISPLAY_NAME = "Tenneo Auth";
|
|
3285
|
+
function AuthCallback({
|
|
3286
|
+
loginPath = "/login",
|
|
3287
|
+
successPath = "/dashboard",
|
|
3288
|
+
homePath = "/"
|
|
3289
|
+
}) {
|
|
3290
|
+
const { setTokens } = useAuthContext();
|
|
3291
|
+
const handledRef = useRef(false);
|
|
3292
|
+
const [status, setStatus] = useState("idle");
|
|
3293
|
+
const [message, setMessage] = useState("");
|
|
3294
|
+
useEffect(() => {
|
|
3295
|
+
if (handledRef.current) return;
|
|
3296
|
+
handledRef.current = true;
|
|
3297
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
3298
|
+
const hasCallbackParams = urlParams.has("code") || urlParams.has("error");
|
|
3299
|
+
if (!hasCallbackParams) {
|
|
3300
|
+
window.location.replace(loginPath || homePath);
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
const error = urlParams.get("error");
|
|
3304
|
+
const errorDescription = urlParams.get("error_description");
|
|
3305
|
+
if (error) {
|
|
3306
|
+
setStatus("error");
|
|
3307
|
+
setMessage(`${error}: ${errorDescription || "Authorization failed"}`);
|
|
3308
|
+
window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
|
|
3309
|
+
return;
|
|
3310
|
+
}
|
|
3311
|
+
const runCallback = async () => {
|
|
3312
|
+
setStatus("loading");
|
|
3313
|
+
setMessage("Completing sign-in...");
|
|
3314
|
+
const handled = await handleCallback();
|
|
3315
|
+
if (!handled) {
|
|
3316
|
+
setStatus("error");
|
|
3317
|
+
setMessage("Authentication failed. Please try again.");
|
|
3318
|
+
window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
const startedAt = Date.now();
|
|
3322
|
+
const MAX_WAIT_MS = 5e3;
|
|
3323
|
+
while (true) {
|
|
3324
|
+
const token = getAccessToken();
|
|
3325
|
+
if (token) {
|
|
3326
|
+
setTokens(token);
|
|
3327
|
+
setStatus("success");
|
|
3328
|
+
setMessage("Authentication successful. Redirecting...");
|
|
3329
|
+
window.location.replace(successPath);
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
if (Date.now() - startedAt > MAX_WAIT_MS) break;
|
|
3333
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
3334
|
+
}
|
|
3335
|
+
setStatus("error");
|
|
3336
|
+
setMessage("Authentication did not complete. Please try again.");
|
|
3337
|
+
window.history.replaceState({}, document.title, window.location.origin + window.location.pathname);
|
|
3338
|
+
};
|
|
3339
|
+
runCallback();
|
|
3340
|
+
}, [homePath, loginPath, successPath, setTokens]);
|
|
3341
|
+
const containerStyle = {
|
|
3342
|
+
minHeight: "200px",
|
|
3343
|
+
display: "flex",
|
|
3344
|
+
flexDirection: "column",
|
|
3345
|
+
alignItems: "center",
|
|
3346
|
+
justifyContent: "center",
|
|
3347
|
+
padding: 32,
|
|
3348
|
+
fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
|
|
3349
|
+
textAlign: "center"
|
|
3350
|
+
};
|
|
3351
|
+
const titleStyle = {
|
|
3352
|
+
margin: 0,
|
|
3353
|
+
fontSize: "1.25rem",
|
|
3354
|
+
fontWeight: 600,
|
|
3355
|
+
color: "#1a1a1a",
|
|
3356
|
+
marginBottom: 8
|
|
3357
|
+
};
|
|
3358
|
+
const messageStyle = {
|
|
3359
|
+
margin: 0,
|
|
3360
|
+
fontSize: "0.9375rem",
|
|
3361
|
+
color: "#555",
|
|
3362
|
+
lineHeight: 1.5,
|
|
3363
|
+
maxWidth: 360
|
|
3364
|
+
};
|
|
3365
|
+
const errorBoxStyle = {
|
|
3366
|
+
marginTop: 16,
|
|
3367
|
+
padding: "12px 16px",
|
|
3368
|
+
backgroundColor: "#fef2f2",
|
|
3369
|
+
border: "1px solid #fecaca",
|
|
3370
|
+
borderRadius: 8,
|
|
3371
|
+
color: "#991b1b",
|
|
3372
|
+
fontSize: "0.875rem",
|
|
3373
|
+
maxWidth: 360
|
|
3374
|
+
};
|
|
3375
|
+
const retryLinkStyle = {
|
|
3376
|
+
marginTop: 16,
|
|
3377
|
+
fontSize: "0.875rem",
|
|
3378
|
+
color: "#2563eb",
|
|
3379
|
+
textDecoration: "none",
|
|
3380
|
+
cursor: "pointer"
|
|
3381
|
+
};
|
|
3382
|
+
const spinnerStyle = {
|
|
3383
|
+
width: 28,
|
|
3384
|
+
height: 28,
|
|
3385
|
+
border: "3px solid #e5e7eb",
|
|
3386
|
+
borderTopColor: "#2563eb",
|
|
3387
|
+
borderRadius: "50%",
|
|
3388
|
+
animation: "auth-callback-spin 0.8s linear infinite",
|
|
3389
|
+
marginBottom: 16
|
|
3390
|
+
};
|
|
3391
|
+
const pluginNameStyle = {
|
|
3392
|
+
position: "absolute",
|
|
3393
|
+
top: 24,
|
|
3394
|
+
left: "50%",
|
|
3395
|
+
transform: "translateX(-50%)",
|
|
3396
|
+
margin: 0,
|
|
3397
|
+
fontSize: "0.875rem",
|
|
3398
|
+
fontWeight: 600,
|
|
3399
|
+
color: "#6b7280",
|
|
3400
|
+
letterSpacing: "0.02em"
|
|
3401
|
+
};
|
|
3402
|
+
return /* @__PURE__ */ jsxs("div", { style: { ...containerStyle, position: "relative" }, children: [
|
|
3403
|
+
/* @__PURE__ */ jsx("style", { children: `@keyframes auth-callback-spin { to { transform: rotate(360deg); } }` }),
|
|
3404
|
+
/* @__PURE__ */ jsx("p", { style: pluginNameStyle, children: PLUGIN_DISPLAY_NAME }),
|
|
3405
|
+
status === "loading" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3406
|
+
/* @__PURE__ */ jsx("div", { style: spinnerStyle, "aria-hidden": true }),
|
|
3407
|
+
/* @__PURE__ */ jsx("h2", { style: titleStyle, children: "Signing you in..." }),
|
|
3408
|
+
/* @__PURE__ */ jsx("p", { style: messageStyle, children: "Please wait while we complete authentication." })
|
|
3409
|
+
] }),
|
|
3410
|
+
status === "success" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3411
|
+
/* @__PURE__ */ jsx("h2", { style: titleStyle, children: "Signed in successfully" }),
|
|
3412
|
+
/* @__PURE__ */ jsx("p", { style: messageStyle, children: "Redirecting you now." })
|
|
3413
|
+
] }),
|
|
3414
|
+
status === "error" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3415
|
+
/* @__PURE__ */ jsx("h2", { style: { ...titleStyle, color: "#991b1b" }, children: "Authentication failed" }),
|
|
3416
|
+
/* @__PURE__ */ jsx("div", { style: errorBoxStyle, children: message }),
|
|
3417
|
+
/* @__PURE__ */ jsx(
|
|
3418
|
+
"a",
|
|
3419
|
+
{
|
|
3420
|
+
href: loginPath,
|
|
3421
|
+
style: retryLinkStyle,
|
|
3422
|
+
onClick: (e) => {
|
|
3423
|
+
e.preventDefault();
|
|
3424
|
+
window.location.href = loginPath;
|
|
3425
|
+
},
|
|
3426
|
+
children: "Try again"
|
|
3427
|
+
}
|
|
3428
|
+
)
|
|
3429
|
+
] }),
|
|
3430
|
+
status === "idle" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3431
|
+
/* @__PURE__ */ jsx("div", { style: spinnerStyle, "aria-hidden": true }),
|
|
3432
|
+
/* @__PURE__ */ jsx("h2", { style: titleStyle, children: "Preparing callback..." }),
|
|
3433
|
+
/* @__PURE__ */ jsx("p", { style: messageStyle, children: "Setting up authentication." })
|
|
3434
|
+
] })
|
|
3435
|
+
] });
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
// src/hooks/useAuth.ts
|
|
3439
|
+
function useAuth() {
|
|
3440
|
+
return useAuthContext();
|
|
3441
|
+
}
|
|
3442
|
+
function ProtectedRoute({
|
|
3443
|
+
children,
|
|
3444
|
+
fallback = null,
|
|
3445
|
+
autoLogin = true
|
|
3446
|
+
}) {
|
|
3447
|
+
const { isAuthenticated, getAccessToken: getAccessToken3, login } = useAuth();
|
|
3448
|
+
const [ready, setReady] = useState(false);
|
|
3449
|
+
const loginTriggeredRef = useRef(false);
|
|
3450
|
+
useEffect(() => {
|
|
3451
|
+
let cancelled = false;
|
|
3452
|
+
const run = async () => {
|
|
3453
|
+
if (isAuthenticated) {
|
|
3454
|
+
if (!cancelled) setReady(true);
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
try {
|
|
3458
|
+
await getAccessToken3();
|
|
3459
|
+
if (!cancelled) setReady(true);
|
|
3460
|
+
} catch {
|
|
3461
|
+
if (autoLogin && !loginTriggeredRef.current) {
|
|
3462
|
+
loginTriggeredRef.current = true;
|
|
3463
|
+
await login();
|
|
3464
|
+
}
|
|
3465
|
+
if (!cancelled) setReady(false);
|
|
3466
|
+
}
|
|
3467
|
+
};
|
|
3468
|
+
run();
|
|
3469
|
+
return () => {
|
|
3470
|
+
cancelled = true;
|
|
3471
|
+
};
|
|
3472
|
+
}, [autoLogin, getAccessToken3, isAuthenticated, login]);
|
|
3473
|
+
if (!ready || !isAuthenticated) {
|
|
3474
|
+
return /* @__PURE__ */ jsx(Fragment, { children: fallback });
|
|
3475
|
+
}
|
|
3476
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
3477
|
+
}
|
|
3478
|
+
function useAuthInit() {
|
|
3479
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
3480
|
+
const [error, setError] = useState(null);
|
|
3481
|
+
const inProgressRef = useRef(false);
|
|
3482
|
+
const mountedRef = useRef(true);
|
|
3483
|
+
useEffect(() => {
|
|
3484
|
+
return () => {
|
|
3485
|
+
mountedRef.current = false;
|
|
3486
|
+
};
|
|
3487
|
+
}, []);
|
|
3488
|
+
const init = useCallback(async () => {
|
|
3489
|
+
if (inProgressRef.current) {
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
inProgressRef.current = true;
|
|
3493
|
+
if (mountedRef.current) {
|
|
3494
|
+
setIsLoading(true);
|
|
3495
|
+
setError(null);
|
|
3496
|
+
}
|
|
3497
|
+
try {
|
|
3498
|
+
await bootstrap();
|
|
3499
|
+
} catch (err) {
|
|
3500
|
+
if (mountedRef.current) {
|
|
3501
|
+
setError(
|
|
3502
|
+
err instanceof Error ? err.message : "Authentication initialization failed"
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
} finally {
|
|
3506
|
+
inProgressRef.current = false;
|
|
3507
|
+
if (mountedRef.current) {
|
|
3508
|
+
setIsLoading(false);
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
}, []);
|
|
3512
|
+
return useMemo(
|
|
3513
|
+
() => ({ isLoading, error, init }),
|
|
3514
|
+
[isLoading, error, init]
|
|
3515
|
+
);
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
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 };
|
|
3519
|
+
//# sourceMappingURL=index.mjs.map
|
|
3520
|
+
//# sourceMappingURL=index.mjs.map
|