@iblai/web-utils 1.10.11 → 1.11.0
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 +48 -0
- package/dist/auth/index.d.ts +64 -2
- package/dist/auth/index.esm.js +137 -1
- package/dist/auth/index.esm.js.map +1 -1
- package/dist/auth/index.js +146 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/web-utils/src/utils/auth.d.ts +62 -0
- package/dist/data-layer/src/features/core/api-slice.d.ts +303 -1
- package/dist/data-layer/src/features/core/types.d.ts +5 -0
- package/dist/data-layer/src/features/user/api-slice.d.ts +426 -1
- package/dist/data-layer/src/features/user/constants.d.ts +5 -0
- package/dist/data-layer/src/features/user/types.d.ts +22 -1
- package/dist/index.d.ts +78 -2
- package/dist/index.esm.js +704 -539
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +713 -537
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/web-utils/src/hooks/index.d.ts +1 -0
- package/dist/web-utils/src/hooks/use-tenant-switch-sync.d.ts +13 -0
- package/dist/web-utils/src/index.web.d.ts +12 -2
- package/dist/web-utils/src/utils/__tests__/tenant-switch.test.d.ts +1 -0
- package/dist/web-utils/src/utils/auth.d.ts +62 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -27,6 +27,535 @@ function _interopNamespaceDefault(e) {
|
|
|
27
27
|
|
|
28
28
|
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Authentication and Authorization Utilities
|
|
32
|
+
*
|
|
33
|
+
* This module provides utility functions for handling authentication flows,
|
|
34
|
+
* including redirects to auth SPA, cookie management, and session handling.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Check if the current window is inside an iframe
|
|
38
|
+
* @returns {boolean} True if running in an iframe, false otherwise
|
|
39
|
+
*/
|
|
40
|
+
function isInIframe() {
|
|
41
|
+
if (typeof window === "undefined") {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return (window === null || window === void 0 ? void 0 : window.self) !== (window === null || window === void 0 ? void 0 : window.top);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Send a message to the parent website (when in iframe)
|
|
48
|
+
* @param payload - The data to send to parent window
|
|
49
|
+
*/
|
|
50
|
+
function sendMessageToParentWebsite(payload) {
|
|
51
|
+
window.parent.postMessage(payload, "*");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Delete a cookie with specific name, path, and domain
|
|
55
|
+
* @param name - Cookie name
|
|
56
|
+
* @param path - Cookie path
|
|
57
|
+
* @param domain - Cookie domain
|
|
58
|
+
*/
|
|
59
|
+
function deleteCookie(name, path, domain) {
|
|
60
|
+
const expires = "expires=Thu, 01 Jan 1970 00:00:00 UTC;";
|
|
61
|
+
const cookieValue = `${name}=;`;
|
|
62
|
+
const pathValue = path ? `path=${path};` : "";
|
|
63
|
+
const domainValue = domain ? `domain=${domain};` : "";
|
|
64
|
+
document.cookie = cookieValue + expires + pathValue + domainValue;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get all possible domain parts for cookie deletion
|
|
68
|
+
* @param domain - The domain to split
|
|
69
|
+
* @returns Array of domain parts
|
|
70
|
+
* @example
|
|
71
|
+
* getDomainParts('app.example.com') // returns ['app.example.com', 'example.com', 'com']
|
|
72
|
+
*/
|
|
73
|
+
function getDomainParts(domain) {
|
|
74
|
+
const parts = domain.split(".");
|
|
75
|
+
const domains = [];
|
|
76
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
77
|
+
domains.push(parts.slice(i).join("."));
|
|
78
|
+
}
|
|
79
|
+
return domains;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Delete a cookie across all possible domain variations
|
|
83
|
+
* @param name - Cookie name to delete
|
|
84
|
+
* @param childDomain - The current domain
|
|
85
|
+
*/
|
|
86
|
+
function deleteCookieOnAllDomains(name, childDomain) {
|
|
87
|
+
getDomainParts(childDomain).forEach((domainPart) => {
|
|
88
|
+
deleteCookie(name, "/", domainPart);
|
|
89
|
+
deleteCookie(name, "", domainPart);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the parent domain from a given domain
|
|
94
|
+
* @param domain - The domain to process
|
|
95
|
+
* @returns The parent domain (e.g., 'app.example.com' → '.example.com')
|
|
96
|
+
*/
|
|
97
|
+
function getParentDomain(domain) {
|
|
98
|
+
if (!domain) {
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
const parts = domain.split(".");
|
|
102
|
+
return parts.length > 1 ? `.${parts.slice(-2).join(".")}` : domain;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Set a cookie for authentication with cross-domain support
|
|
106
|
+
* @param name - Cookie name
|
|
107
|
+
* @param value - Cookie value
|
|
108
|
+
* @param days - Number of days until expiration (default: 365)
|
|
109
|
+
*/
|
|
110
|
+
function setCookieForAuth(name, value, days = 365) {
|
|
111
|
+
const expires = new Date();
|
|
112
|
+
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
|
|
113
|
+
const hostname = window.location.hostname;
|
|
114
|
+
let baseDomain = hostname;
|
|
115
|
+
// Calculate base domain (skip for localhost and IP addresses)
|
|
116
|
+
if (hostname !== "localhost" && !/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
|
|
117
|
+
const parts = hostname.split(".");
|
|
118
|
+
if (parts.length > 2) {
|
|
119
|
+
baseDomain = `.${parts.slice(-2).join(".")}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const domainAttr = baseDomain ? `;domain=${baseDomain}` : "";
|
|
123
|
+
document.cookie = `${name}=${encodeURIComponent(value)};expires=${expires.toUTCString()};path=/;SameSite=None;Secure${domainAttr}`;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Clear all cookies for the current domain
|
|
127
|
+
*/
|
|
128
|
+
function clearCookies() {
|
|
129
|
+
const cookies = document.cookie.split(";");
|
|
130
|
+
for (let i = 0; i < cookies.length; i++) {
|
|
131
|
+
const cookie = cookies[i];
|
|
132
|
+
const eqPos = cookie.indexOf("=");
|
|
133
|
+
const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
|
|
134
|
+
deleteCookieOnAllDomains(name, window.location.hostname);
|
|
135
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;Domain=${getParentDomain(window.location.hostname)}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get the value of a cookie by name
|
|
140
|
+
* @param name - Cookie name
|
|
141
|
+
* @returns The cookie value or null if not found
|
|
142
|
+
*/
|
|
143
|
+
function getCookieValue(name) {
|
|
144
|
+
const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
145
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Check if user is currently logged in
|
|
149
|
+
* @param tokenKey - The localStorage key for the auth token (default: 'axd_token')
|
|
150
|
+
* @returns True if logged in, false otherwise
|
|
151
|
+
*/
|
|
152
|
+
function isLoggedIn(tokenKey = "axd_token") {
|
|
153
|
+
return !!localStorage.getItem(tokenKey);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Extract platform/tenant key from URL
|
|
157
|
+
* @param url - The URL to parse
|
|
158
|
+
* @param pattern - RegExp pattern to match platform key (default matches /platform/[key]/...)
|
|
159
|
+
* @returns The platform key or null if not found
|
|
160
|
+
*/
|
|
161
|
+
function getPlatformKey(url, pattern = /\/platform\/([^/]+)/) {
|
|
162
|
+
const match = url.match(pattern);
|
|
163
|
+
return match ? match[1] : null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Redirect to authentication SPA for login/logout
|
|
167
|
+
*
|
|
168
|
+
* This function handles the complete authentication flow:
|
|
169
|
+
* - Clears localStorage and cookies on logout
|
|
170
|
+
* - Saves redirect path for post-auth return
|
|
171
|
+
* - Handles iframe scenarios
|
|
172
|
+
* - Syncs logout across multiple SPAs via cookies
|
|
173
|
+
*
|
|
174
|
+
* @param options - Configuration options for the redirect
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```tsx
|
|
178
|
+
* // Basic usage
|
|
179
|
+
* redirectToAuthSpa({
|
|
180
|
+
* authUrl: 'https://auth.example.com',
|
|
181
|
+
* appName: 'mentor',
|
|
182
|
+
* });
|
|
183
|
+
*
|
|
184
|
+
* // With logout
|
|
185
|
+
* redirectToAuthSpa({
|
|
186
|
+
* authUrl: 'https://auth.example.com',
|
|
187
|
+
* appName: 'mentor',
|
|
188
|
+
* logout: true,
|
|
189
|
+
* platformKey: 'my-tenant',
|
|
190
|
+
* });
|
|
191
|
+
*
|
|
192
|
+
* // With custom redirect
|
|
193
|
+
* redirectToAuthSpa({
|
|
194
|
+
* authUrl: 'https://auth.example.com',
|
|
195
|
+
* appName: 'mentor',
|
|
196
|
+
* redirectTo: '/dashboard',
|
|
197
|
+
* platformKey: 'my-tenant',
|
|
198
|
+
* });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
async function redirectToAuthSpa(options) {
|
|
202
|
+
const { redirectTo, platformKey, logout = false, saveRedirect = true, authUrl, appName, queryParams = {
|
|
203
|
+
app: "app",
|
|
204
|
+
redirectTo: "redirect-to",
|
|
205
|
+
tenant: "tenant",
|
|
206
|
+
}, redirectPathStorageKey = "redirect_to", cookieNames = {
|
|
207
|
+
currentTenant: "ibl_current_tenant",
|
|
208
|
+
userData: "ibl_user_data",
|
|
209
|
+
tenant: "ibl_tenant",
|
|
210
|
+
logoutTimestamp: "ibl_logout_timestamp",
|
|
211
|
+
loginTimestamp: "ibl_login_timestamp",
|
|
212
|
+
tenantSwitching: "ibl_tenant_switching",
|
|
213
|
+
}, hasNonExpiredAuthToken, isOffline, preserveTokenKey, authRedirectProxy, isNativeApp, scheme = "iblai-mentor", forceRedirect = false, } = options;
|
|
214
|
+
console.log("[redirectToAuthSpa] starting redirect to auth spa", redirectTo, platformKey, logout, saveRedirect, forceRedirect);
|
|
215
|
+
// Skip if a tenant switch is already in progress
|
|
216
|
+
if (!forceRedirect &&
|
|
217
|
+
cookieNames.tenantSwitching &&
|
|
218
|
+
document.cookie.includes(cookieNames.tenantSwitching)) {
|
|
219
|
+
console.log("[redirectToAuthSpa] Tenant switch in progress, skipping redirect");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// Skip if a login occurred after the last logout (login takes precedence)
|
|
223
|
+
// but only if this app actually has a valid auth token
|
|
224
|
+
if (!forceRedirect &&
|
|
225
|
+
hasNonExpiredAuthToken &&
|
|
226
|
+
cookieNames.loginTimestamp &&
|
|
227
|
+
cookieNames.logoutTimestamp) {
|
|
228
|
+
const loginTs = getCookieValue(cookieNames.loginTimestamp);
|
|
229
|
+
const logoutTs = getCookieValue(cookieNames.logoutTimestamp);
|
|
230
|
+
const hasValidToken = hasNonExpiredAuthToken();
|
|
231
|
+
console.log("[redirectToAuthSpa] Login/logout timestamp check", {
|
|
232
|
+
loginTs,
|
|
233
|
+
logoutTs,
|
|
234
|
+
hasValidToken,
|
|
235
|
+
loginAhead: loginTs && logoutTs ? Number(loginTs) > Number(logoutTs) : false,
|
|
236
|
+
});
|
|
237
|
+
if (!forceRedirect &&
|
|
238
|
+
hasValidToken &&
|
|
239
|
+
loginTs &&
|
|
240
|
+
logoutTs &&
|
|
241
|
+
Number(loginTs) > Number(logoutTs)) {
|
|
242
|
+
console.log("[redirectToAuthSpa] Login timestamp is ahead of logout timestamp, skipping redirect", { loginTs, logoutTs });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Skip redirect in offline mode
|
|
247
|
+
if (isOffline === null || isOffline === void 0 ? void 0 : isOffline()) {
|
|
248
|
+
console.log("[redirectToAuthSpa] Skipping redirect - offline mode");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Preserve a token before clearing localStorage if requested
|
|
252
|
+
const preservedToken = preserveTokenKey
|
|
253
|
+
? window.localStorage.getItem(preserveTokenKey)
|
|
254
|
+
: null;
|
|
255
|
+
console.log("[redirectToAuthSpa] clearing local storage");
|
|
256
|
+
localStorage.clear();
|
|
257
|
+
if (logout || isInIframe()) {
|
|
258
|
+
// Delete authentication cookies for cross-SPA synchronization
|
|
259
|
+
const currentDomain = window.location.hostname;
|
|
260
|
+
if (cookieNames.currentTenant) {
|
|
261
|
+
deleteCookieOnAllDomains(cookieNames.currentTenant, currentDomain);
|
|
262
|
+
}
|
|
263
|
+
if (cookieNames.userData) {
|
|
264
|
+
deleteCookieOnAllDomains(cookieNames.userData, currentDomain);
|
|
265
|
+
}
|
|
266
|
+
if (cookieNames.tenant) {
|
|
267
|
+
deleteCookieOnAllDomains(cookieNames.tenant, currentDomain);
|
|
268
|
+
}
|
|
269
|
+
// Set logout timestamp cookie to trigger logout on other SPAs
|
|
270
|
+
if (cookieNames.logoutTimestamp && !isInIframe()) {
|
|
271
|
+
setCookieForAuth(cookieNames.logoutTimestamp, Date.now().toString());
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Handle iframe scenario
|
|
275
|
+
if (isInIframe()) {
|
|
276
|
+
console.log("[redirectToAuthSpa]: sending authExpired to parent");
|
|
277
|
+
sendMessageToParentWebsite({ authExpired: true });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const redirectPath = redirectTo !== null && redirectTo !== void 0 ? redirectTo : `${window.location.pathname}${window.location.search}`;
|
|
281
|
+
// Never save sso-login routes as redirect paths
|
|
282
|
+
if (!redirectPath.startsWith("/sso-login") &&
|
|
283
|
+
!redirectPath.startsWith("/sso-login-complete") &&
|
|
284
|
+
saveRedirect) {
|
|
285
|
+
window.localStorage.setItem(redirectPathStorageKey, redirectPath);
|
|
286
|
+
}
|
|
287
|
+
const platform = platformKey !== null && platformKey !== void 0 ? platformKey : getPlatformKey(redirectPath);
|
|
288
|
+
const redirectToUrl = window.location.origin;
|
|
289
|
+
let authRedirectUrl = `${authUrl}/login?${queryParams.app}=${appName}`;
|
|
290
|
+
authRedirectUrl += `&${queryParams.redirectTo}=${redirectToUrl}`;
|
|
291
|
+
if (platform) {
|
|
292
|
+
authRedirectUrl += `&${queryParams.tenant}=${platform}`;
|
|
293
|
+
}
|
|
294
|
+
if (logout) {
|
|
295
|
+
authRedirectUrl += "&logout=1";
|
|
296
|
+
}
|
|
297
|
+
if ((isNativeApp === null || isNativeApp === void 0 ? void 0 : isNativeApp()) && scheme) {
|
|
298
|
+
authRedirectUrl += `&scheme=${scheme}`;
|
|
299
|
+
}
|
|
300
|
+
// Small delay for any pending operations
|
|
301
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
302
|
+
if (isNativeApp === null || isNativeApp === void 0 ? void 0 : isNativeApp()) {
|
|
303
|
+
// On native apps (e.g., Tauri), pass preserved token and navigate directly
|
|
304
|
+
if (preservedToken && preserveTokenKey) {
|
|
305
|
+
authRedirectUrl += `&token=${encodeURIComponent(preservedToken)}`;
|
|
306
|
+
console.log("[redirectToAuthSpa] Added preserved token to auth URL");
|
|
307
|
+
}
|
|
308
|
+
console.log("[redirectToAuthSpa] Native app, navigating to auth URL:", authRedirectUrl);
|
|
309
|
+
window.location.href = authRedirectUrl;
|
|
310
|
+
}
|
|
311
|
+
else if (authRedirectProxy) {
|
|
312
|
+
// Use auth redirect proxy endpoint
|
|
313
|
+
window.location.href = `${authRedirectProxy}?to=${encodeURIComponent(authRedirectUrl)}`;
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
window.location.href = authRedirectUrl;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Get the URL for joining a tenant (sign up flow)
|
|
321
|
+
* @param authUrl - Auth SPA base URL
|
|
322
|
+
* @param tenantKey - Tenant to join
|
|
323
|
+
* @param redirectUrl - URL to redirect to after joining (defaults to current URL)
|
|
324
|
+
* @returns The join URL
|
|
325
|
+
*/
|
|
326
|
+
function getAuthSpaJoinUrl(authUrl, tenantKey, redirectUrl) {
|
|
327
|
+
const resolvedTenant = tenantKey || getPlatformKey(window.location.pathname) || "";
|
|
328
|
+
if (!resolvedTenant) {
|
|
329
|
+
return "";
|
|
330
|
+
}
|
|
331
|
+
const targetUrl = redirectUrl !== null && redirectUrl !== void 0 ? redirectUrl : window.location.href;
|
|
332
|
+
const joinUrl = `${authUrl}/join?tenant=${encodeURIComponent(resolvedTenant)}&redirect-to=${encodeURIComponent(targetUrl)}`;
|
|
333
|
+
return joinUrl;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Redirect to auth SPA's join/signup page for a specific tenant
|
|
337
|
+
* @param authUrl - Auth SPA base URL
|
|
338
|
+
* @param tenantKey - Tenant to join
|
|
339
|
+
* @param redirectUrl - URL to redirect to after joining (defaults to current URL)
|
|
340
|
+
*/
|
|
341
|
+
function redirectToAuthSpaJoinTenant(authUrl, tenantKey, redirectUrl) {
|
|
342
|
+
const resolvedTenant = tenantKey || getPlatformKey(window.location.pathname) || "";
|
|
343
|
+
if (!resolvedTenant) {
|
|
344
|
+
console.log("[auth-redirect] Missing tenant key for join", {
|
|
345
|
+
tenantKey,
|
|
346
|
+
redirectUrl,
|
|
347
|
+
});
|
|
348
|
+
redirectToAuthSpa({
|
|
349
|
+
authUrl,
|
|
350
|
+
appName: "app", // Will need to be configured
|
|
351
|
+
redirectTo: redirectUrl,
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const targetUrl = redirectUrl !== null && redirectUrl !== void 0 ? redirectUrl : window.location.href;
|
|
356
|
+
const joinUrl = `${authUrl}/join?tenant=${encodeURIComponent(resolvedTenant)}&redirect-to=${encodeURIComponent(targetUrl)}`;
|
|
357
|
+
window.location.href = joinUrl;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Handle user logout
|
|
361
|
+
*
|
|
362
|
+
* This function:
|
|
363
|
+
* - Clears localStorage (preserving tenant)
|
|
364
|
+
* - Sets logout timestamp cookie for cross-SPA sync
|
|
365
|
+
* - Clears all cookies
|
|
366
|
+
* - Redirects to auth SPA logout endpoint
|
|
367
|
+
*
|
|
368
|
+
* @param options - Logout configuration options
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* ```tsx
|
|
372
|
+
* handleLogout({
|
|
373
|
+
* authUrl: 'https://auth.example.com',
|
|
374
|
+
* redirectUrl: 'https://app.example.com',
|
|
375
|
+
* });
|
|
376
|
+
* ```
|
|
377
|
+
*/
|
|
378
|
+
function handleLogout(options) {
|
|
379
|
+
const { redirectUrl = window.location.origin, authUrl, tenantStorageKey = "tenant", logoutTimestampCookie = "ibl_logout_timestamp", callback, } = options;
|
|
380
|
+
const tenant = window.localStorage.getItem(tenantStorageKey);
|
|
381
|
+
window.localStorage.clear();
|
|
382
|
+
if (tenant) {
|
|
383
|
+
window.localStorage.setItem(tenantStorageKey, tenant);
|
|
384
|
+
}
|
|
385
|
+
// Set logout timestamp cookie to trigger logout on other SPAs
|
|
386
|
+
setCookieForAuth(logoutTimestampCookie, Date.now().toString());
|
|
387
|
+
clearCookies();
|
|
388
|
+
callback === null || callback === void 0 ? void 0 : callback();
|
|
389
|
+
if (!isInIframe()) {
|
|
390
|
+
window.location.href = `${authUrl}/logout?redirect-to=${redirectUrl}${tenant ? "&tenant=" + tenant : ""}`;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// ── Tenant-switch coordination ─────────────────────────────────────────────
|
|
394
|
+
// Cross-tab tenant switching: a TTL lock (shared via localStorage) plus a
|
|
395
|
+
// BroadcastChannel signal let multiple tabs coordinate a switch and prevent the
|
|
396
|
+
// "ping-pong" where a stale tab reverts the session to its own route's tenant.
|
|
397
|
+
const TENANT_SWITCH_CHANNEL = "ibl-tenant-switch";
|
|
398
|
+
const TENANT_SWITCH_LOCK_KEY = "ibl_tenant_switch_lock";
|
|
399
|
+
const DEFAULT_TENANT_SWITCH_LOCK_TTL_MS = 8000;
|
|
400
|
+
let _tabId = null;
|
|
401
|
+
/**
|
|
402
|
+
* Stable per-tab id. Uses sessionStorage so it survives same-tab reloads while
|
|
403
|
+
* staying unique per tab/window.
|
|
404
|
+
*/
|
|
405
|
+
function getTabId() {
|
|
406
|
+
var _a, _b;
|
|
407
|
+
if (_tabId)
|
|
408
|
+
return _tabId;
|
|
409
|
+
try {
|
|
410
|
+
const existing = sessionStorage.getItem("ibl_tab_id");
|
|
411
|
+
if (existing)
|
|
412
|
+
return (_tabId = existing);
|
|
413
|
+
const id = (_b = (_a = crypto === null || crypto === void 0 ? void 0 : crypto.randomUUID) === null || _a === void 0 ? void 0 : _a.call(crypto)) !== null && _b !== void 0 ? _b : `${Date.now()}-${Math.random()}`;
|
|
414
|
+
sessionStorage.setItem("ibl_tab_id", id);
|
|
415
|
+
return (_tabId = id);
|
|
416
|
+
}
|
|
417
|
+
catch (_c) {
|
|
418
|
+
return (_tabId = `${Date.now()}-${Math.random()}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/** The active (non-expired) tenant-switch lock, or null. */
|
|
422
|
+
function readTenantSwitchLock(ttlMs = DEFAULT_TENANT_SWITCH_LOCK_TTL_MS) {
|
|
423
|
+
try {
|
|
424
|
+
const raw = localStorage.getItem(TENANT_SWITCH_LOCK_KEY);
|
|
425
|
+
if (!raw)
|
|
426
|
+
return null;
|
|
427
|
+
const lock = JSON.parse(raw);
|
|
428
|
+
if (typeof (lock === null || lock === void 0 ? void 0 : lock.ts) !== "number")
|
|
429
|
+
return null;
|
|
430
|
+
if (Date.now() - lock.ts > ttlMs)
|
|
431
|
+
return null;
|
|
432
|
+
return lock;
|
|
433
|
+
}
|
|
434
|
+
catch (_a) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/** Write a fresh lock for `tenant` (newest intent). Returns the lock written. */
|
|
439
|
+
function writeTenantSwitchLock(tenant) {
|
|
440
|
+
const lock = {
|
|
441
|
+
tenant,
|
|
442
|
+
token: `${Date.now()}-${getTabId()}`,
|
|
443
|
+
ts: Date.now(),
|
|
444
|
+
senderId: getTabId(),
|
|
445
|
+
};
|
|
446
|
+
try {
|
|
447
|
+
localStorage.setItem(TENANT_SWITCH_LOCK_KEY, JSON.stringify(lock));
|
|
448
|
+
}
|
|
449
|
+
catch (_a) { }
|
|
450
|
+
return lock;
|
|
451
|
+
}
|
|
452
|
+
/** Extend an existing lock's window from "now" (no-op when no active lock). */
|
|
453
|
+
function refreshTenantSwitchLock() {
|
|
454
|
+
const lock = readTenantSwitchLock();
|
|
455
|
+
if (!lock)
|
|
456
|
+
return;
|
|
457
|
+
try {
|
|
458
|
+
localStorage.setItem(TENANT_SWITCH_LOCK_KEY, JSON.stringify({ ...lock, ts: Date.now() }));
|
|
459
|
+
}
|
|
460
|
+
catch (_a) { }
|
|
461
|
+
}
|
|
462
|
+
function clearTenantSwitchLock() {
|
|
463
|
+
try {
|
|
464
|
+
localStorage.removeItem(TENANT_SWITCH_LOCK_KEY);
|
|
465
|
+
}
|
|
466
|
+
catch (_a) { }
|
|
467
|
+
}
|
|
468
|
+
/** True while a tenant switch is in flight — gate automatic reverts on this. */
|
|
469
|
+
function isTenantSwitchInProgress(ttlMs) {
|
|
470
|
+
return readTenantSwitchLock(ttlMs) !== null;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Switch the active tenant. Coordinates across tabs (TTL lock + BroadcastChannel),
|
|
474
|
+
* clears storage, then redirects through the auth SPA's `/login/complete`.
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* await handleTenantSwitch("acme", {
|
|
478
|
+
* authUrl: config.authUrl(),
|
|
479
|
+
* clearCurrentTenantCookie,
|
|
480
|
+
* });
|
|
481
|
+
*/
|
|
482
|
+
async function handleTenantSwitch(tenant, options) {
|
|
483
|
+
var _a, _b, _c;
|
|
484
|
+
const { authUrl, redirectPathStorageKey = "redirect-to", preserveTokenKey = "edx_jwt_token", tenantStorageKey = "tenant", tenantSwitchingCookie = "ibl_tenant_switching", queryParams = {}, saveRedirect = false, redirectUrl, broadcast = true, clearCurrentTenantCookie, } = options;
|
|
485
|
+
const tenantParam = (_a = queryParams.tenant) !== null && _a !== void 0 ? _a : "tenant";
|
|
486
|
+
const redirectToParam = (_b = queryParams.redirectTo) !== null && _b !== void 0 ? _b : "redirect-to";
|
|
487
|
+
// Guard: don't start a new user switch while one is already in progress.
|
|
488
|
+
if (broadcast && document.cookie.includes(tenantSwitchingCookie))
|
|
489
|
+
return;
|
|
490
|
+
// Guard: already on the target tenant.
|
|
491
|
+
if (tenant === localStorage.getItem(tenantStorageKey))
|
|
492
|
+
return;
|
|
493
|
+
if (broadcast) {
|
|
494
|
+
setCookieForAuth(tenantSwitchingCookie, "true");
|
|
495
|
+
writeTenantSwitchLock(tenant);
|
|
496
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
497
|
+
const channel = new BroadcastChannel(TENANT_SWITCH_CHANNEL);
|
|
498
|
+
const lock = readTenantSwitchLock(options.lockTtlMs);
|
|
499
|
+
channel.postMessage({
|
|
500
|
+
type: "TENANT_SWITCHING",
|
|
501
|
+
tenant,
|
|
502
|
+
senderId: (_c = lock === null || lock === void 0 ? void 0 : lock.senderId) !== null && _c !== void 0 ? _c : getTabId(),
|
|
503
|
+
token: lock === null || lock === void 0 ? void 0 : lock.token,
|
|
504
|
+
});
|
|
505
|
+
channel.close();
|
|
506
|
+
}
|
|
507
|
+
clearCurrentTenantCookie === null || clearCurrentTenantCookie === void 0 ? void 0 : clearCurrentTenantCookie();
|
|
508
|
+
}
|
|
509
|
+
const currentPath = `${window.location.pathname}${window.location.search}`;
|
|
510
|
+
const jwtToken = localStorage.getItem(preserveTokenKey);
|
|
511
|
+
if (broadcast)
|
|
512
|
+
localStorage.clear();
|
|
513
|
+
const params = {
|
|
514
|
+
[tenantParam]: tenant,
|
|
515
|
+
[redirectToParam]: redirectUrl !== null && redirectUrl !== void 0 ? redirectUrl : window.location.origin,
|
|
516
|
+
};
|
|
517
|
+
if (jwtToken)
|
|
518
|
+
params.token = jwtToken;
|
|
519
|
+
localStorage.setItem(tenantStorageKey, tenant);
|
|
520
|
+
if (saveRedirect)
|
|
521
|
+
localStorage.setItem(redirectPathStorageKey, currentPath);
|
|
522
|
+
// Re-persist: localStorage.clear() above wiped the pre-broadcast lock; this
|
|
523
|
+
// copy survives the redirect so stale tabs honor the switch window.
|
|
524
|
+
if (broadcast)
|
|
525
|
+
writeTenantSwitchLock(tenant);
|
|
526
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
527
|
+
window.location.href = `${authUrl}/login/complete?${new URLSearchParams(params)}`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Subscribe to cross-tab tenant-switch broadcasts. Ignores this tab's own echo
|
|
532
|
+
* (BroadcastChannel delivers to sibling instances in the same tab) and refreshes
|
|
533
|
+
* the shared switch lock so this tab won't auto-revert during the switch window.
|
|
534
|
+
*
|
|
535
|
+
* @example
|
|
536
|
+
* useTenantSwitchSync();
|
|
537
|
+
*/
|
|
538
|
+
function useTenantSwitchSync(options = {}) {
|
|
539
|
+
const onRemoteSwitchRef = React.useRef(options.onRemoteSwitch);
|
|
540
|
+
onRemoteSwitchRef.current = options.onRemoteSwitch;
|
|
541
|
+
React.useEffect(() => {
|
|
542
|
+
if (typeof BroadcastChannel === "undefined")
|
|
543
|
+
return;
|
|
544
|
+
const channel = new BroadcastChannel(TENANT_SWITCH_CHANNEL);
|
|
545
|
+
channel.onmessage = (event) => {
|
|
546
|
+
var _a, _b;
|
|
547
|
+
const { type, tenant, senderId } = (_a = event.data) !== null && _a !== void 0 ? _a : {};
|
|
548
|
+
if (type !== "TENANT_SWITCHING")
|
|
549
|
+
return;
|
|
550
|
+
if (senderId && senderId === getTabId())
|
|
551
|
+
return; // ignore our own echo
|
|
552
|
+
refreshTenantSwitchLock();
|
|
553
|
+
(_b = onRemoteSwitchRef.current) === null || _b === void 0 ? void 0 : _b.call(onRemoteSwitchRef, tenant);
|
|
554
|
+
};
|
|
555
|
+
return () => channel.close();
|
|
556
|
+
}, []);
|
|
557
|
+
}
|
|
558
|
+
|
|
30
559
|
const SUBSCRIPTION_TRIGGERS = {
|
|
31
560
|
PRICING_MODAL: "TRIGGER_PRICING_MODAL",
|
|
32
561
|
SUBSCRIBE_USER: "TRIGGER_SUBSCRIBE_USER",
|
|
@@ -820,568 +1349,204 @@ const TOOL_NAME_MAP = {
|
|
|
820
1349
|
vector_search: "Searching knowledge base",
|
|
821
1350
|
calculator: "Calculating",
|
|
822
1351
|
file_reader: "Reading file",
|
|
823
|
-
code_executor: "Running code",
|
|
824
|
-
wikipedia: "Searching Wikipedia",
|
|
825
|
-
};
|
|
826
|
-
const REQUIRED_ACTIONS_FOR_GROUPS = {
|
|
827
|
-
NOTIFICATIONS: "Ibl.Notifications/Notification/action",
|
|
828
|
-
};
|
|
829
|
-
const MENTOR_VISIBILITY_VALUES = {
|
|
830
|
-
ADMINISTRATORS: "viewable_by_tenant_admins",
|
|
831
|
-
STUDENTS: "viewable_by_tenant_students",
|
|
832
|
-
ANYONE: "viewable_by_anyone",
|
|
833
|
-
};
|
|
834
|
-
const MENTOR_VISIBILITY = [
|
|
835
|
-
{
|
|
836
|
-
label: "Administrators",
|
|
837
|
-
value: MENTOR_VISIBILITY_VALUES.ADMINISTRATORS,
|
|
838
|
-
},
|
|
839
|
-
{
|
|
840
|
-
label: "Students",
|
|
841
|
-
value: MENTOR_VISIBILITY_VALUES.STUDENTS,
|
|
842
|
-
},
|
|
843
|
-
{
|
|
844
|
-
label: "Anyone",
|
|
845
|
-
value: MENTOR_VISIBILITY_VALUES.ANYONE,
|
|
846
|
-
},
|
|
847
|
-
];
|
|
848
|
-
|
|
849
|
-
/**
|
|
850
|
-
* Reads the cached user_nicename from the LOCAL_STORAGE_KEYS.USER_DATA blob.
|
|
851
|
-
*/
|
|
852
|
-
const getUserName = () => {
|
|
853
|
-
var _a;
|
|
854
|
-
if (typeof window === "undefined" ||
|
|
855
|
-
typeof (localStorage === null || localStorage === void 0 ? void 0 : localStorage.getItem) !== "function")
|
|
856
|
-
return null;
|
|
857
|
-
try {
|
|
858
|
-
return (_a = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEYS.USER_DATA))) === null || _a === void 0 ? void 0 : _a.user_nicename;
|
|
859
|
-
}
|
|
860
|
-
catch (_b) {
|
|
861
|
-
return null;
|
|
862
|
-
}
|
|
863
|
-
};
|
|
864
|
-
/**
|
|
865
|
-
* Reads the cached user_email from the LOCAL_STORAGE_KEYS.USER_DATA blob.
|
|
866
|
-
*/
|
|
867
|
-
const getUserEmail = () => {
|
|
868
|
-
var _a;
|
|
869
|
-
if (typeof window === "undefined" ||
|
|
870
|
-
typeof (localStorage === null || localStorage === void 0 ? void 0 : localStorage.getItem) !== "function")
|
|
871
|
-
return null;
|
|
872
|
-
try {
|
|
873
|
-
return (_a = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEYS.USER_DATA))) === null || _a === void 0 ? void 0 : _a.user_email;
|
|
874
|
-
}
|
|
875
|
-
catch (_b) {
|
|
876
|
-
return null;
|
|
877
|
-
}
|
|
878
|
-
};
|
|
879
|
-
/**
|
|
880
|
-
* Whether Stripe-backed billing is active for the given tenant. The Stripe
|
|
881
|
-
* feature flag is a host-side config value; pass it in. Tenants opt in via
|
|
882
|
-
* `show_paywall` or by being the "main" platform tenant.
|
|
883
|
-
*/
|
|
884
|
-
function isStripeActivated(currentTenant, stripeEnabled) {
|
|
885
|
-
return Boolean(stripeEnabled &&
|
|
886
|
-
((currentTenant === null || currentTenant === void 0 ? void 0 : currentTenant.show_paywall) || (currentTenant === null || currentTenant === void 0 ? void 0 : currentTenant.key) === "main"));
|
|
887
|
-
}
|
|
888
|
-
/**
|
|
889
|
-
* Checks whether a file matches any of the accepted type specifiers.
|
|
890
|
-
* Supports MIME types (image/png), wildcard MIME (image/*), and extensions (.pdf).
|
|
891
|
-
*/
|
|
892
|
-
function isFileAccepted(file, acceptedTypes) {
|
|
893
|
-
if (acceptedTypes.length === 0)
|
|
894
|
-
return true;
|
|
895
|
-
return acceptedTypes.some((type) => {
|
|
896
|
-
if (type.startsWith(".")) {
|
|
897
|
-
return file.name.toLowerCase().endsWith(type.toLowerCase());
|
|
898
|
-
}
|
|
899
|
-
if (type.endsWith("/*")) {
|
|
900
|
-
const category = type.split("/")[0];
|
|
901
|
-
return file.type.startsWith(category + "/");
|
|
902
|
-
}
|
|
903
|
-
return file.type === type;
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
const isJSON = (text) => {
|
|
907
|
-
if (typeof text !== "string") {
|
|
908
|
-
return false;
|
|
909
|
-
}
|
|
910
|
-
try {
|
|
911
|
-
JSON.parse(text);
|
|
912
|
-
return true;
|
|
913
|
-
}
|
|
914
|
-
catch (_a) {
|
|
915
|
-
return false;
|
|
916
|
-
}
|
|
917
|
-
};
|
|
918
|
-
const getInitials = (fullName) => {
|
|
919
|
-
const names = fullName.split(" ");
|
|
920
|
-
if (names.length === 1) {
|
|
921
|
-
return names[0].substring(0, 2).toUpperCase();
|
|
922
|
-
}
|
|
923
|
-
return names
|
|
924
|
-
.slice(0, 2)
|
|
925
|
-
.map((name) => name.charAt(0).toUpperCase())
|
|
926
|
-
.join("");
|
|
927
|
-
};
|
|
928
|
-
const ALPHANUMERIC_32_REGEX = /^[a-zA-Z0-9]{32}$/;
|
|
929
|
-
const isAlphaNumeric32 = (text) => {
|
|
930
|
-
return ALPHANUMERIC_32_REGEX.test(text);
|
|
931
|
-
};
|
|
932
|
-
const addProtocolToUrl = (url) => {
|
|
933
|
-
if (!url.startsWith("http")) {
|
|
934
|
-
return `https://${url}`;
|
|
935
|
-
}
|
|
936
|
-
return url;
|
|
937
|
-
};
|
|
938
|
-
function getTimeAgo(createdAt) {
|
|
939
|
-
const now = new Date();
|
|
940
|
-
const created = new Date(createdAt);
|
|
941
|
-
const diffInMilliseconds = now.getTime() - created.getTime();
|
|
942
|
-
const diffInMinutes = Math.floor(diffInMilliseconds / (1000 * 60));
|
|
943
|
-
const diffInHours = Math.floor(diffInMilliseconds / (1000 * 60 * 60));
|
|
944
|
-
const diffInDays = Math.floor(diffInHours / 24);
|
|
945
|
-
if (diffInMinutes < 60) {
|
|
946
|
-
return `${diffInMinutes} min ago`;
|
|
947
|
-
}
|
|
948
|
-
else if (diffInHours < 24) {
|
|
949
|
-
return `${diffInHours} hrs ago`;
|
|
950
|
-
}
|
|
951
|
-
else {
|
|
952
|
-
return `${diffInDays} ${diffInDays === 1 ? "day" : "days"} ago`;
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
// Utility function to format relative time
|
|
956
|
-
const formatRelativeTime = (timestamp) => {
|
|
957
|
-
const now = new Date();
|
|
958
|
-
const date = new Date(timestamp);
|
|
959
|
-
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
960
|
-
if (diffInSeconds < 60) {
|
|
961
|
-
return "Just now";
|
|
962
|
-
}
|
|
963
|
-
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
|
964
|
-
if (diffInMinutes < 60) {
|
|
965
|
-
return `${diffInMinutes} minute${diffInMinutes === 1 ? "" : "s"} ago`;
|
|
966
|
-
}
|
|
967
|
-
const diffInHours = Math.floor(diffInMinutes / 60);
|
|
968
|
-
if (diffInHours < 24) {
|
|
969
|
-
return `${diffInHours} hour${diffInHours === 1 ? "" : "s"} ago`;
|
|
970
|
-
}
|
|
971
|
-
const diffInDays = Math.floor(diffInHours / 24);
|
|
972
|
-
if (diffInDays < 7) {
|
|
973
|
-
return `${diffInDays} day${diffInDays === 1 ? "" : "s"} ago`;
|
|
974
|
-
}
|
|
975
|
-
const diffInWeeks = Math.floor(diffInDays / 7);
|
|
976
|
-
if (diffInWeeks < 4) {
|
|
977
|
-
return `${diffInWeeks} week${diffInWeeks === 1 ? "" : "s"} ago`;
|
|
978
|
-
}
|
|
979
|
-
const diffInMonths = Math.floor(diffInDays / 30);
|
|
980
|
-
if (diffInMonths < 12) {
|
|
981
|
-
return `${diffInMonths} month${diffInMonths === 1 ? "" : "s"} ago`;
|
|
982
|
-
}
|
|
983
|
-
const diffInYears = Math.floor(diffInDays / 365);
|
|
984
|
-
return `${diffInYears} year${diffInYears === 1 ? "" : "s"} ago`;
|
|
985
|
-
};
|
|
986
|
-
/**
|
|
987
|
-
* Convert markdown text to plain text by removing markdown formatting
|
|
988
|
-
* @param text - The markdown text to convert
|
|
989
|
-
* @returns Plain text without markdown formatting
|
|
990
|
-
*/
|
|
991
|
-
const markdownToPlainText = (text) => {
|
|
992
|
-
return (text
|
|
993
|
-
// Remove code blocks first (before other replacements)
|
|
994
|
-
.replace(/```[\s\S]*?```/g, "")
|
|
995
|
-
// Remove headers (# ## ### etc)
|
|
996
|
-
.replace(/^#{1,6}\s+/gm, "")
|
|
997
|
-
// Remove images  - must come before links
|
|
998
|
-
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
|
|
999
|
-
// Remove links [text](url) - keep only the text
|
|
1000
|
-
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
1001
|
-
// Remove horizontal rules (---, ***, ___) - must come before bold/italic
|
|
1002
|
-
.replace(/^[-*_]{3,}\s*$/gm, "")
|
|
1003
|
-
// Remove bold (**text** or __text__)
|
|
1004
|
-
.replace(/(\*\*|__)(.*?)\1/g, "$2")
|
|
1005
|
-
// Remove italic (*text* or _text_) - only single * or _
|
|
1006
|
-
.replace(/(?<!\*)\*(?!\*)([^*]+)\*(?!\*)/g, "$1")
|
|
1007
|
-
.replace(/(?<!_)_(?!_)([^_]+)_(?!_)/g, "$1")
|
|
1008
|
-
// Remove strikethrough (~~text~~)
|
|
1009
|
-
.replace(/~~(.*?)~~/g, "$1")
|
|
1010
|
-
// Remove inline code (`code`)
|
|
1011
|
-
.replace(/`([^`]+)`/g, "$1")
|
|
1012
|
-
// Remove blockquotes (> text)
|
|
1013
|
-
.replace(/^>\s+/gm, "")
|
|
1014
|
-
// Remove list markers (- * + 1. 2. etc)
|
|
1015
|
-
.replace(/^[\s]*[-*+]\s+/gm, "")
|
|
1016
|
-
.replace(/^[\s]*\d+\.\s+/gm, "")
|
|
1017
|
-
// Clean up multiple spaces
|
|
1018
|
-
.replace(/\s+/g, " ")
|
|
1019
|
-
.trim());
|
|
1352
|
+
code_executor: "Running code",
|
|
1353
|
+
wikipedia: "Searching Wikipedia",
|
|
1354
|
+
};
|
|
1355
|
+
const REQUIRED_ACTIONS_FOR_GROUPS = {
|
|
1356
|
+
NOTIFICATIONS: "Ibl.Notifications/Notification/action",
|
|
1357
|
+
};
|
|
1358
|
+
const MENTOR_VISIBILITY_VALUES = {
|
|
1359
|
+
ADMINISTRATORS: "viewable_by_tenant_admins",
|
|
1360
|
+
STUDENTS: "viewable_by_tenant_students",
|
|
1361
|
+
ANYONE: "viewable_by_anyone",
|
|
1020
1362
|
};
|
|
1363
|
+
const MENTOR_VISIBILITY = [
|
|
1364
|
+
{
|
|
1365
|
+
label: "Administrators",
|
|
1366
|
+
value: MENTOR_VISIBILITY_VALUES.ADMINISTRATORS,
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
label: "Users",
|
|
1370
|
+
value: MENTOR_VISIBILITY_VALUES.STUDENTS,
|
|
1371
|
+
},
|
|
1372
|
+
{
|
|
1373
|
+
label: "Anyone",
|
|
1374
|
+
value: MENTOR_VISIBILITY_VALUES.ANYONE,
|
|
1375
|
+
},
|
|
1376
|
+
];
|
|
1021
1377
|
|
|
1022
1378
|
/**
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
* This module provides utility functions for handling authentication flows,
|
|
1026
|
-
* including redirects to auth SPA, cookie management, and session handling.
|
|
1027
|
-
*/
|
|
1028
|
-
/**
|
|
1029
|
-
* Check if the current window is inside an iframe
|
|
1030
|
-
* @returns {boolean} True if running in an iframe, false otherwise
|
|
1031
|
-
*/
|
|
1032
|
-
function isInIframe() {
|
|
1033
|
-
if (typeof window === "undefined") {
|
|
1034
|
-
return false;
|
|
1035
|
-
}
|
|
1036
|
-
return (window === null || window === void 0 ? void 0 : window.self) !== (window === null || window === void 0 ? void 0 : window.top);
|
|
1037
|
-
}
|
|
1038
|
-
/**
|
|
1039
|
-
* Send a message to the parent website (when in iframe)
|
|
1040
|
-
* @param payload - The data to send to parent window
|
|
1041
|
-
*/
|
|
1042
|
-
function sendMessageToParentWebsite(payload) {
|
|
1043
|
-
window.parent.postMessage(payload, "*");
|
|
1044
|
-
}
|
|
1045
|
-
/**
|
|
1046
|
-
* Delete a cookie with specific name, path, and domain
|
|
1047
|
-
* @param name - Cookie name
|
|
1048
|
-
* @param path - Cookie path
|
|
1049
|
-
* @param domain - Cookie domain
|
|
1050
|
-
*/
|
|
1051
|
-
function deleteCookie(name, path, domain) {
|
|
1052
|
-
const expires = "expires=Thu, 01 Jan 1970 00:00:00 UTC;";
|
|
1053
|
-
const cookieValue = `${name}=;`;
|
|
1054
|
-
const pathValue = path ? `path=${path};` : "";
|
|
1055
|
-
const domainValue = domain ? `domain=${domain};` : "";
|
|
1056
|
-
document.cookie = cookieValue + expires + pathValue + domainValue;
|
|
1057
|
-
}
|
|
1058
|
-
/**
|
|
1059
|
-
* Get all possible domain parts for cookie deletion
|
|
1060
|
-
* @param domain - The domain to split
|
|
1061
|
-
* @returns Array of domain parts
|
|
1062
|
-
* @example
|
|
1063
|
-
* getDomainParts('app.example.com') // returns ['app.example.com', 'example.com', 'com']
|
|
1064
|
-
*/
|
|
1065
|
-
function getDomainParts(domain) {
|
|
1066
|
-
const parts = domain.split(".");
|
|
1067
|
-
const domains = [];
|
|
1068
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
1069
|
-
domains.push(parts.slice(i).join("."));
|
|
1070
|
-
}
|
|
1071
|
-
return domains;
|
|
1072
|
-
}
|
|
1073
|
-
/**
|
|
1074
|
-
* Delete a cookie across all possible domain variations
|
|
1075
|
-
* @param name - Cookie name to delete
|
|
1076
|
-
* @param childDomain - The current domain
|
|
1077
|
-
*/
|
|
1078
|
-
function deleteCookieOnAllDomains(name, childDomain) {
|
|
1079
|
-
getDomainParts(childDomain).forEach((domainPart) => {
|
|
1080
|
-
deleteCookie(name, "/", domainPart);
|
|
1081
|
-
deleteCookie(name, "", domainPart);
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
|
-
/**
|
|
1085
|
-
* Get the parent domain from a given domain
|
|
1086
|
-
* @param domain - The domain to process
|
|
1087
|
-
* @returns The parent domain (e.g., 'app.example.com' → '.example.com')
|
|
1088
|
-
*/
|
|
1089
|
-
function getParentDomain(domain) {
|
|
1090
|
-
if (!domain) {
|
|
1091
|
-
return "";
|
|
1092
|
-
}
|
|
1093
|
-
const parts = domain.split(".");
|
|
1094
|
-
return parts.length > 1 ? `.${parts.slice(-2).join(".")}` : domain;
|
|
1095
|
-
}
|
|
1096
|
-
/**
|
|
1097
|
-
* Set a cookie for authentication with cross-domain support
|
|
1098
|
-
* @param name - Cookie name
|
|
1099
|
-
* @param value - Cookie value
|
|
1100
|
-
* @param days - Number of days until expiration (default: 365)
|
|
1379
|
+
* Reads the cached user_nicename from the LOCAL_STORAGE_KEYS.USER_DATA blob.
|
|
1101
1380
|
*/
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
const parts = hostname.split(".");
|
|
1110
|
-
if (parts.length > 2) {
|
|
1111
|
-
baseDomain = `.${parts.slice(-2).join(".")}`;
|
|
1112
|
-
}
|
|
1381
|
+
const getUserName = () => {
|
|
1382
|
+
var _a;
|
|
1383
|
+
if (typeof window === "undefined" ||
|
|
1384
|
+
typeof (localStorage === null || localStorage === void 0 ? void 0 : localStorage.getItem) !== "function")
|
|
1385
|
+
return null;
|
|
1386
|
+
try {
|
|
1387
|
+
return (_a = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEYS.USER_DATA))) === null || _a === void 0 ? void 0 : _a.user_nicename;
|
|
1113
1388
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
}
|
|
1117
|
-
/**
|
|
1118
|
-
* Clear all cookies for the current domain
|
|
1119
|
-
*/
|
|
1120
|
-
function clearCookies() {
|
|
1121
|
-
const cookies = document.cookie.split(";");
|
|
1122
|
-
for (let i = 0; i < cookies.length; i++) {
|
|
1123
|
-
const cookie = cookies[i];
|
|
1124
|
-
const eqPos = cookie.indexOf("=");
|
|
1125
|
-
const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
|
|
1126
|
-
deleteCookieOnAllDomains(name, window.location.hostname);
|
|
1127
|
-
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;Domain=${getParentDomain(window.location.hostname)}`;
|
|
1389
|
+
catch (_b) {
|
|
1390
|
+
return null;
|
|
1128
1391
|
}
|
|
1129
|
-
}
|
|
1130
|
-
/**
|
|
1131
|
-
* Get the value of a cookie by name
|
|
1132
|
-
* @param name - Cookie name
|
|
1133
|
-
* @returns The cookie value or null if not found
|
|
1134
|
-
*/
|
|
1135
|
-
function getCookieValue(name) {
|
|
1136
|
-
const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
1137
|
-
return match ? decodeURIComponent(match[1]) : null;
|
|
1138
|
-
}
|
|
1139
|
-
/**
|
|
1140
|
-
* Check if user is currently logged in
|
|
1141
|
-
* @param tokenKey - The localStorage key for the auth token (default: 'axd_token')
|
|
1142
|
-
* @returns True if logged in, false otherwise
|
|
1143
|
-
*/
|
|
1144
|
-
function isLoggedIn(tokenKey = "axd_token") {
|
|
1145
|
-
return !!localStorage.getItem(tokenKey);
|
|
1146
|
-
}
|
|
1147
|
-
/**
|
|
1148
|
-
* Extract platform/tenant key from URL
|
|
1149
|
-
* @param url - The URL to parse
|
|
1150
|
-
* @param pattern - RegExp pattern to match platform key (default matches /platform/[key]/...)
|
|
1151
|
-
* @returns The platform key or null if not found
|
|
1152
|
-
*/
|
|
1153
|
-
function getPlatformKey(url, pattern = /\/platform\/([^/]+)/) {
|
|
1154
|
-
const match = url.match(pattern);
|
|
1155
|
-
return match ? match[1] : null;
|
|
1156
|
-
}
|
|
1392
|
+
};
|
|
1157
1393
|
/**
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
* This function handles the complete authentication flow:
|
|
1161
|
-
* - Clears localStorage and cookies on logout
|
|
1162
|
-
* - Saves redirect path for post-auth return
|
|
1163
|
-
* - Handles iframe scenarios
|
|
1164
|
-
* - Syncs logout across multiple SPAs via cookies
|
|
1165
|
-
*
|
|
1166
|
-
* @param options - Configuration options for the redirect
|
|
1167
|
-
*
|
|
1168
|
-
* @example
|
|
1169
|
-
* ```tsx
|
|
1170
|
-
* // Basic usage
|
|
1171
|
-
* redirectToAuthSpa({
|
|
1172
|
-
* authUrl: 'https://auth.example.com',
|
|
1173
|
-
* appName: 'mentor',
|
|
1174
|
-
* });
|
|
1175
|
-
*
|
|
1176
|
-
* // With logout
|
|
1177
|
-
* redirectToAuthSpa({
|
|
1178
|
-
* authUrl: 'https://auth.example.com',
|
|
1179
|
-
* appName: 'mentor',
|
|
1180
|
-
* logout: true,
|
|
1181
|
-
* platformKey: 'my-tenant',
|
|
1182
|
-
* });
|
|
1183
|
-
*
|
|
1184
|
-
* // With custom redirect
|
|
1185
|
-
* redirectToAuthSpa({
|
|
1186
|
-
* authUrl: 'https://auth.example.com',
|
|
1187
|
-
* appName: 'mentor',
|
|
1188
|
-
* redirectTo: '/dashboard',
|
|
1189
|
-
* platformKey: 'my-tenant',
|
|
1190
|
-
* });
|
|
1191
|
-
* ```
|
|
1394
|
+
* Reads the cached user_email from the LOCAL_STORAGE_KEYS.USER_DATA blob.
|
|
1192
1395
|
*/
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
userData: "ibl_user_data",
|
|
1201
|
-
tenant: "ibl_tenant",
|
|
1202
|
-
logoutTimestamp: "ibl_logout_timestamp",
|
|
1203
|
-
loginTimestamp: "ibl_login_timestamp",
|
|
1204
|
-
tenantSwitching: "ibl_tenant_switching",
|
|
1205
|
-
}, hasNonExpiredAuthToken, isOffline, preserveTokenKey, authRedirectProxy, isNativeApp, scheme = "iblai-mentor", forceRedirect = false, } = options;
|
|
1206
|
-
console.log("[redirectToAuthSpa] starting redirect to auth spa", redirectTo, platformKey, logout, saveRedirect, forceRedirect);
|
|
1207
|
-
// Skip if a tenant switch is already in progress
|
|
1208
|
-
if (!forceRedirect &&
|
|
1209
|
-
cookieNames.tenantSwitching &&
|
|
1210
|
-
document.cookie.includes(cookieNames.tenantSwitching)) {
|
|
1211
|
-
console.log("[redirectToAuthSpa] Tenant switch in progress, skipping redirect");
|
|
1212
|
-
return;
|
|
1213
|
-
}
|
|
1214
|
-
// Skip if a login occurred after the last logout (login takes precedence)
|
|
1215
|
-
// but only if this app actually has a valid auth token
|
|
1216
|
-
if (!forceRedirect &&
|
|
1217
|
-
hasNonExpiredAuthToken &&
|
|
1218
|
-
cookieNames.loginTimestamp &&
|
|
1219
|
-
cookieNames.logoutTimestamp) {
|
|
1220
|
-
const loginTs = getCookieValue(cookieNames.loginTimestamp);
|
|
1221
|
-
const logoutTs = getCookieValue(cookieNames.logoutTimestamp);
|
|
1222
|
-
const hasValidToken = hasNonExpiredAuthToken();
|
|
1223
|
-
console.log("[redirectToAuthSpa] Login/logout timestamp check", {
|
|
1224
|
-
loginTs,
|
|
1225
|
-
logoutTs,
|
|
1226
|
-
hasValidToken,
|
|
1227
|
-
loginAhead: loginTs && logoutTs ? Number(loginTs) > Number(logoutTs) : false,
|
|
1228
|
-
});
|
|
1229
|
-
if (!forceRedirect &&
|
|
1230
|
-
hasValidToken &&
|
|
1231
|
-
loginTs &&
|
|
1232
|
-
logoutTs &&
|
|
1233
|
-
Number(loginTs) > Number(logoutTs)) {
|
|
1234
|
-
console.log("[redirectToAuthSpa] Login timestamp is ahead of logout timestamp, skipping redirect", { loginTs, logoutTs });
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
// Skip redirect in offline mode
|
|
1239
|
-
if (isOffline === null || isOffline === void 0 ? void 0 : isOffline()) {
|
|
1240
|
-
console.log("[redirectToAuthSpa] Skipping redirect - offline mode");
|
|
1241
|
-
return;
|
|
1396
|
+
const getUserEmail = () => {
|
|
1397
|
+
var _a;
|
|
1398
|
+
if (typeof window === "undefined" ||
|
|
1399
|
+
typeof (localStorage === null || localStorage === void 0 ? void 0 : localStorage.getItem) !== "function")
|
|
1400
|
+
return null;
|
|
1401
|
+
try {
|
|
1402
|
+
return (_a = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEYS.USER_DATA))) === null || _a === void 0 ? void 0 : _a.user_email;
|
|
1242
1403
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1404
|
+
catch (_b) {
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
/**
|
|
1409
|
+
* Whether Stripe-backed billing is active for the given tenant. The Stripe
|
|
1410
|
+
* feature flag is a host-side config value; pass it in. Tenants opt in via
|
|
1411
|
+
* `show_paywall` or by being the "main" platform tenant.
|
|
1412
|
+
*/
|
|
1413
|
+
function isStripeActivated(currentTenant, stripeEnabled) {
|
|
1414
|
+
return Boolean(stripeEnabled &&
|
|
1415
|
+
((currentTenant === null || currentTenant === void 0 ? void 0 : currentTenant.show_paywall) || (currentTenant === null || currentTenant === void 0 ? void 0 : currentTenant.key) === "main"));
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Checks whether a file matches any of the accepted type specifiers.
|
|
1419
|
+
* Supports MIME types (image/png), wildcard MIME (image/*), and extensions (.pdf).
|
|
1420
|
+
*/
|
|
1421
|
+
function isFileAccepted(file, acceptedTypes) {
|
|
1422
|
+
if (acceptedTypes.length === 0)
|
|
1423
|
+
return true;
|
|
1424
|
+
return acceptedTypes.some((type) => {
|
|
1425
|
+
if (type.startsWith(".")) {
|
|
1426
|
+
return file.name.toLowerCase().endsWith(type.toLowerCase());
|
|
1260
1427
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1428
|
+
if (type.endsWith("/*")) {
|
|
1429
|
+
const category = type.split("/")[0];
|
|
1430
|
+
return file.type.startsWith(category + "/");
|
|
1264
1431
|
}
|
|
1432
|
+
return file.type === type;
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
const isJSON = (text) => {
|
|
1436
|
+
if (typeof text !== "string") {
|
|
1437
|
+
return false;
|
|
1265
1438
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
sendMessageToParentWebsite({ authExpired: true });
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
const redirectPath = redirectTo !== null && redirectTo !== void 0 ? redirectTo : `${window.location.pathname}${window.location.search}`;
|
|
1273
|
-
// Never save sso-login routes as redirect paths
|
|
1274
|
-
if (!redirectPath.startsWith("/sso-login") &&
|
|
1275
|
-
!redirectPath.startsWith("/sso-login-complete") &&
|
|
1276
|
-
saveRedirect) {
|
|
1277
|
-
window.localStorage.setItem(redirectPathStorageKey, redirectPath);
|
|
1439
|
+
try {
|
|
1440
|
+
JSON.parse(text);
|
|
1441
|
+
return true;
|
|
1278
1442
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
let authRedirectUrl = `${authUrl}/login?${queryParams.app}=${appName}`;
|
|
1282
|
-
authRedirectUrl += `&${queryParams.redirectTo}=${redirectToUrl}`;
|
|
1283
|
-
if (platform) {
|
|
1284
|
-
authRedirectUrl += `&${queryParams.tenant}=${platform}`;
|
|
1443
|
+
catch (_a) {
|
|
1444
|
+
return false;
|
|
1285
1445
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1446
|
+
};
|
|
1447
|
+
const getInitials = (fullName) => {
|
|
1448
|
+
const names = fullName.split(" ");
|
|
1449
|
+
if (names.length === 1) {
|
|
1450
|
+
return names[0].substring(0, 2).toUpperCase();
|
|
1288
1451
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1452
|
+
return names
|
|
1453
|
+
.slice(0, 2)
|
|
1454
|
+
.map((name) => name.charAt(0).toUpperCase())
|
|
1455
|
+
.join("");
|
|
1456
|
+
};
|
|
1457
|
+
const ALPHANUMERIC_32_REGEX = /^[a-zA-Z0-9]{32}$/;
|
|
1458
|
+
const isAlphaNumeric32 = (text) => {
|
|
1459
|
+
return ALPHANUMERIC_32_REGEX.test(text);
|
|
1460
|
+
};
|
|
1461
|
+
const addProtocolToUrl = (url) => {
|
|
1462
|
+
if (!url.startsWith("http")) {
|
|
1463
|
+
return `https://${url}`;
|
|
1291
1464
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1465
|
+
return url;
|
|
1466
|
+
};
|
|
1467
|
+
function getTimeAgo(createdAt) {
|
|
1468
|
+
const now = new Date();
|
|
1469
|
+
const created = new Date(createdAt);
|
|
1470
|
+
const diffInMilliseconds = now.getTime() - created.getTime();
|
|
1471
|
+
const diffInMinutes = Math.floor(diffInMilliseconds / (1000 * 60));
|
|
1472
|
+
const diffInHours = Math.floor(diffInMilliseconds / (1000 * 60 * 60));
|
|
1473
|
+
const diffInDays = Math.floor(diffInHours / 24);
|
|
1474
|
+
if (diffInMinutes < 60) {
|
|
1475
|
+
return `${diffInMinutes} min ago`;
|
|
1302
1476
|
}
|
|
1303
|
-
else if (
|
|
1304
|
-
|
|
1305
|
-
window.location.href = `${authRedirectProxy}?to=${encodeURIComponent(authRedirectUrl)}`;
|
|
1477
|
+
else if (diffInHours < 24) {
|
|
1478
|
+
return `${diffInHours} hrs ago`;
|
|
1306
1479
|
}
|
|
1307
1480
|
else {
|
|
1308
|
-
|
|
1481
|
+
return `${diffInDays} ${diffInDays === 1 ? "day" : "days"} ago`;
|
|
1309
1482
|
}
|
|
1310
1483
|
}
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
function getAuthSpaJoinUrl(authUrl, tenantKey, redirectUrl) {
|
|
1319
|
-
const resolvedTenant = tenantKey || getPlatformKey(window.location.pathname) || "";
|
|
1320
|
-
if (!resolvedTenant) {
|
|
1321
|
-
return "";
|
|
1484
|
+
// Utility function to format relative time
|
|
1485
|
+
const formatRelativeTime = (timestamp) => {
|
|
1486
|
+
const now = new Date();
|
|
1487
|
+
const date = new Date(timestamp);
|
|
1488
|
+
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
1489
|
+
if (diffInSeconds < 60) {
|
|
1490
|
+
return "Just now";
|
|
1322
1491
|
}
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
}
|
|
1327
|
-
/**
|
|
1328
|
-
* Redirect to auth SPA's join/signup page for a specific tenant
|
|
1329
|
-
* @param authUrl - Auth SPA base URL
|
|
1330
|
-
* @param tenantKey - Tenant to join
|
|
1331
|
-
* @param redirectUrl - URL to redirect to after joining (defaults to current URL)
|
|
1332
|
-
*/
|
|
1333
|
-
function redirectToAuthSpaJoinTenant(authUrl, tenantKey, redirectUrl) {
|
|
1334
|
-
const resolvedTenant = tenantKey || getPlatformKey(window.location.pathname) || "";
|
|
1335
|
-
if (!resolvedTenant) {
|
|
1336
|
-
console.log("[auth-redirect] Missing tenant key for join", {
|
|
1337
|
-
tenantKey,
|
|
1338
|
-
redirectUrl,
|
|
1339
|
-
});
|
|
1340
|
-
redirectToAuthSpa({
|
|
1341
|
-
authUrl,
|
|
1342
|
-
appName: "app", // Will need to be configured
|
|
1343
|
-
redirectTo: redirectUrl,
|
|
1344
|
-
});
|
|
1345
|
-
return;
|
|
1492
|
+
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
|
1493
|
+
if (diffInMinutes < 60) {
|
|
1494
|
+
return `${diffInMinutes} minute${diffInMinutes === 1 ? "" : "s"} ago`;
|
|
1346
1495
|
}
|
|
1347
|
-
const
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
}
|
|
1351
|
-
/**
|
|
1352
|
-
* Handle user logout
|
|
1353
|
-
*
|
|
1354
|
-
* This function:
|
|
1355
|
-
* - Clears localStorage (preserving tenant)
|
|
1356
|
-
* - Sets logout timestamp cookie for cross-SPA sync
|
|
1357
|
-
* - Clears all cookies
|
|
1358
|
-
* - Redirects to auth SPA logout endpoint
|
|
1359
|
-
*
|
|
1360
|
-
* @param options - Logout configuration options
|
|
1361
|
-
*
|
|
1362
|
-
* @example
|
|
1363
|
-
* ```tsx
|
|
1364
|
-
* handleLogout({
|
|
1365
|
-
* authUrl: 'https://auth.example.com',
|
|
1366
|
-
* redirectUrl: 'https://app.example.com',
|
|
1367
|
-
* });
|
|
1368
|
-
* ```
|
|
1369
|
-
*/
|
|
1370
|
-
function handleLogout(options) {
|
|
1371
|
-
const { redirectUrl = window.location.origin, authUrl, tenantStorageKey = "tenant", logoutTimestampCookie = "ibl_logout_timestamp", callback, } = options;
|
|
1372
|
-
const tenant = window.localStorage.getItem(tenantStorageKey);
|
|
1373
|
-
window.localStorage.clear();
|
|
1374
|
-
if (tenant) {
|
|
1375
|
-
window.localStorage.setItem(tenantStorageKey, tenant);
|
|
1496
|
+
const diffInHours = Math.floor(diffInMinutes / 60);
|
|
1497
|
+
if (diffInHours < 24) {
|
|
1498
|
+
return `${diffInHours} hour${diffInHours === 1 ? "" : "s"} ago`;
|
|
1376
1499
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
callback === null || callback === void 0 ? void 0 : callback();
|
|
1381
|
-
if (!isInIframe()) {
|
|
1382
|
-
window.location.href = `${authUrl}/logout?redirect-to=${redirectUrl}${tenant ? "&tenant=" + tenant : ""}`;
|
|
1500
|
+
const diffInDays = Math.floor(diffInHours / 24);
|
|
1501
|
+
if (diffInDays < 7) {
|
|
1502
|
+
return `${diffInDays} day${diffInDays === 1 ? "" : "s"} ago`;
|
|
1383
1503
|
}
|
|
1384
|
-
|
|
1504
|
+
const diffInWeeks = Math.floor(diffInDays / 7);
|
|
1505
|
+
if (diffInWeeks < 4) {
|
|
1506
|
+
return `${diffInWeeks} week${diffInWeeks === 1 ? "" : "s"} ago`;
|
|
1507
|
+
}
|
|
1508
|
+
const diffInMonths = Math.floor(diffInDays / 30);
|
|
1509
|
+
if (diffInMonths < 12) {
|
|
1510
|
+
return `${diffInMonths} month${diffInMonths === 1 ? "" : "s"} ago`;
|
|
1511
|
+
}
|
|
1512
|
+
const diffInYears = Math.floor(diffInDays / 365);
|
|
1513
|
+
return `${diffInYears} year${diffInYears === 1 ? "" : "s"} ago`;
|
|
1514
|
+
};
|
|
1515
|
+
/**
|
|
1516
|
+
* Convert markdown text to plain text by removing markdown formatting
|
|
1517
|
+
* @param text - The markdown text to convert
|
|
1518
|
+
* @returns Plain text without markdown formatting
|
|
1519
|
+
*/
|
|
1520
|
+
const markdownToPlainText = (text) => {
|
|
1521
|
+
return (text
|
|
1522
|
+
// Remove code blocks first (before other replacements)
|
|
1523
|
+
.replace(/```[\s\S]*?```/g, "")
|
|
1524
|
+
// Remove headers (# ## ### etc)
|
|
1525
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
1526
|
+
// Remove images  - must come before links
|
|
1527
|
+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
|
|
1528
|
+
// Remove links [text](url) - keep only the text
|
|
1529
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
1530
|
+
// Remove horizontal rules (---, ***, ___) - must come before bold/italic
|
|
1531
|
+
.replace(/^[-*_]{3,}\s*$/gm, "")
|
|
1532
|
+
// Remove bold (**text** or __text__)
|
|
1533
|
+
.replace(/(\*\*|__)(.*?)\1/g, "$2")
|
|
1534
|
+
// Remove italic (*text* or _text_) - only single * or _
|
|
1535
|
+
.replace(/(?<!\*)\*(?!\*)([^*]+)\*(?!\*)/g, "$1")
|
|
1536
|
+
.replace(/(?<!_)_(?!_)([^_]+)_(?!_)/g, "$1")
|
|
1537
|
+
// Remove strikethrough (~~text~~)
|
|
1538
|
+
.replace(/~~(.*?)~~/g, "$1")
|
|
1539
|
+
// Remove inline code (`code`)
|
|
1540
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
1541
|
+
// Remove blockquotes (> text)
|
|
1542
|
+
.replace(/^>\s+/gm, "")
|
|
1543
|
+
// Remove list markers (- * + 1. 2. etc)
|
|
1544
|
+
.replace(/^[\s]*[-*+]\s+/gm, "")
|
|
1545
|
+
.replace(/^[\s]*\d+\.\s+/gm, "")
|
|
1546
|
+
// Clean up multiple spaces
|
|
1547
|
+
.replace(/\s+/g, " ")
|
|
1548
|
+
.trim());
|
|
1549
|
+
};
|
|
1385
1550
|
|
|
1386
1551
|
// Platform detection utility
|
|
1387
1552
|
const isReactNative = () => {
|
|
@@ -18236,6 +18401,7 @@ exports.AuthProvider = AuthProvider;
|
|
|
18236
18401
|
exports.CHAT_AREA_SIZE = CHAT_AREA_SIZE;
|
|
18237
18402
|
exports.CacheKeys = CacheKeys;
|
|
18238
18403
|
exports.DEFAULT_DISCLAIMER_CONTENT = DEFAULT_DISCLAIMER_CONTENT;
|
|
18404
|
+
exports.DEFAULT_TENANT_SWITCH_LOCK_TTL_MS = DEFAULT_TENANT_SWITCH_LOCK_TTL_MS;
|
|
18239
18405
|
exports.LOCAL_STORAGE_KEYS = LOCAL_STORAGE_KEYS;
|
|
18240
18406
|
exports.MAX_INITIAL_WEBSOCKET_CONNECTION_ATTEMPTS = MAX_INITIAL_WEBSOCKET_CONNECTION_ATTEMPTS;
|
|
18241
18407
|
exports.MENTOR_CHAT_DOCUMENTS_EXTENSIONS = MENTOR_CHAT_DOCUMENTS_EXTENSIONS;
|
|
@@ -18254,6 +18420,8 @@ exports.SUBSCRIPTION_V2_TRIGGERS = SUBSCRIPTION_V2_TRIGGERS;
|
|
|
18254
18420
|
exports.ServiceWorkerProvider = ServiceWorkerProvider;
|
|
18255
18421
|
exports.SubscriptionFlow = SubscriptionFlow;
|
|
18256
18422
|
exports.SubscriptionFlowV2 = SubscriptionFlowV2;
|
|
18423
|
+
exports.TENANT_SWITCH_CHANNEL = TENANT_SWITCH_CHANNEL;
|
|
18424
|
+
exports.TENANT_SWITCH_LOCK_KEY = TENANT_SWITCH_LOCK_KEY;
|
|
18257
18425
|
exports.TOOLS = TOOLS;
|
|
18258
18426
|
exports.TOOL_NAME_MAP = TOOL_NAME_MAP;
|
|
18259
18427
|
exports.TenantContext = TenantContext;
|
|
@@ -18285,6 +18453,7 @@ exports.clearCookies = clearCookies;
|
|
|
18285
18453
|
exports.clearCurrentTenantCookie = clearCurrentTenantCookie;
|
|
18286
18454
|
exports.clearFiles = clearFiles;
|
|
18287
18455
|
exports.clearMessages = clearMessages;
|
|
18456
|
+
exports.clearTenantSwitchLock = clearTenantSwitchLock;
|
|
18288
18457
|
exports.combineCSVData = combineCSVData;
|
|
18289
18458
|
exports.convertToOllamaMessages = convertToOllamaMessages;
|
|
18290
18459
|
exports.createFileReference = createFileReference;
|
|
@@ -18313,10 +18482,12 @@ exports.getPlatform = getPlatform;
|
|
|
18313
18482
|
exports.getPlatformKey = getPlatformKey;
|
|
18314
18483
|
exports.getServiceWorkerStatus = getServiceWorkerStatus;
|
|
18315
18484
|
exports.getStoredUserName = getStoredUserName;
|
|
18485
|
+
exports.getTabId = getTabId;
|
|
18316
18486
|
exports.getTimeAgo = getTimeAgo;
|
|
18317
18487
|
exports.getUserEmail = getUserEmail;
|
|
18318
18488
|
exports.getUserName = getUserName;
|
|
18319
18489
|
exports.handleLogout = handleLogout;
|
|
18490
|
+
exports.handleTenantSwitch = handleTenantSwitch;
|
|
18320
18491
|
exports.hasNonExpiredAuthToken = hasNonExpiredAuthToken;
|
|
18321
18492
|
exports.hostChatReducer = hostChatReducer;
|
|
18322
18493
|
exports.hostChatSlice = hostChatSlice;
|
|
@@ -18334,6 +18505,7 @@ exports.isServiceWorkerSupported = isServiceWorkerSupported;
|
|
|
18334
18505
|
exports.isStripeActivated = isStripeActivated;
|
|
18335
18506
|
exports.isTauri = isTauri;
|
|
18336
18507
|
exports.isTauriOfflineMode = isTauriOfflineMode;
|
|
18508
|
+
exports.isTenantSwitchInProgress = isTenantSwitchInProgress;
|
|
18337
18509
|
exports.isWeb = isWeb$2;
|
|
18338
18510
|
exports.loadMetadataConfig = loadMetadataConfig;
|
|
18339
18511
|
exports.markdownToPlainText = markdownToPlainText;
|
|
@@ -18343,8 +18515,10 @@ exports.onUpdate = onUpdate;
|
|
|
18343
18515
|
exports.parseCSV = parseCSV;
|
|
18344
18516
|
exports.preCacheMentorData = preCacheMentorData;
|
|
18345
18517
|
exports.rbacReducer = rbacReducer;
|
|
18518
|
+
exports.readTenantSwitchLock = readTenantSwitchLock;
|
|
18346
18519
|
exports.redirectToAuthSpa = redirectToAuthSpa;
|
|
18347
18520
|
exports.redirectToAuthSpaJoinTenant = redirectToAuthSpaJoinTenant;
|
|
18521
|
+
exports.refreshTenantSwitchLock = refreshTenantSwitchLock;
|
|
18348
18522
|
exports.registerServiceWorker = registerServiceWorker;
|
|
18349
18523
|
exports.removeFile = removeFile;
|
|
18350
18524
|
exports.requestPresignedUrl = requestPresignedUrl;
|
|
@@ -18453,6 +18627,7 @@ exports.useSubscriptionHandler = useSubscriptionHandler;
|
|
|
18453
18627
|
exports.useSubscriptionHandlerV2 = useSubscriptionHandlerV2;
|
|
18454
18628
|
exports.useTenantContext = useTenantContext;
|
|
18455
18629
|
exports.useTenantMetadata = useTenantMetadata;
|
|
18630
|
+
exports.useTenantSwitchSync = useTenantSwitchSync;
|
|
18456
18631
|
exports.useTimeTracker = useTimeTracker;
|
|
18457
18632
|
exports.useTimeTrackerNative = useTimeTrackerNative;
|
|
18458
18633
|
exports.useTimer = useTimer;
|
|
@@ -18466,4 +18641,5 @@ exports.useVoiceChat = useVoiceChat;
|
|
|
18466
18641
|
exports.useWelcomeMessage = useWelcome;
|
|
18467
18642
|
exports.userDataSchema = userDataSchema;
|
|
18468
18643
|
exports.validateFile = validateFile;
|
|
18644
|
+
exports.writeTenantSwitchLock = writeTenantSwitchLock;
|
|
18469
18645
|
//# sourceMappingURL=index.js.map
|