@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/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 ![alt](url) - 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
- * Authentication and Authorization Utilities
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
- function setCookieForAuth(name, value, days = 365) {
1103
- const expires = new Date();
1104
- expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
1105
- const hostname = window.location.hostname;
1106
- let baseDomain = hostname;
1107
- // Calculate base domain (skip for localhost and IP addresses)
1108
- if (hostname !== "localhost" && !/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
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
- const domainAttr = baseDomain ? `;domain=${baseDomain}` : "";
1115
- document.cookie = `${name}=${encodeURIComponent(value)};expires=${expires.toUTCString()};path=/;SameSite=None;Secure${domainAttr}`;
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
- * Redirect to authentication SPA for login/logout
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
- async function redirectToAuthSpa(options) {
1194
- const { redirectTo, platformKey, logout = false, saveRedirect = true, authUrl, appName, queryParams = {
1195
- app: "app",
1196
- redirectTo: "redirect-to",
1197
- tenant: "tenant",
1198
- }, redirectPathStorageKey = "redirect_to", cookieNames = {
1199
- currentTenant: "ibl_current_tenant",
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
- // Preserve a token before clearing localStorage if requested
1244
- const preservedToken = preserveTokenKey
1245
- ? window.localStorage.getItem(preserveTokenKey)
1246
- : null;
1247
- console.log("[redirectToAuthSpa] clearing local storage");
1248
- localStorage.clear();
1249
- if (logout || isInIframe()) {
1250
- // Delete authentication cookies for cross-SPA synchronization
1251
- const currentDomain = window.location.hostname;
1252
- if (cookieNames.currentTenant) {
1253
- deleteCookieOnAllDomains(cookieNames.currentTenant, currentDomain);
1254
- }
1255
- if (cookieNames.userData) {
1256
- deleteCookieOnAllDomains(cookieNames.userData, currentDomain);
1257
- }
1258
- if (cookieNames.tenant) {
1259
- deleteCookieOnAllDomains(cookieNames.tenant, currentDomain);
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
- // Set logout timestamp cookie to trigger logout on other SPAs
1262
- if (cookieNames.logoutTimestamp && !isInIframe()) {
1263
- setCookieForAuth(cookieNames.logoutTimestamp, Date.now().toString());
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
- // Handle iframe scenario
1267
- if (isInIframe()) {
1268
- console.log("[redirectToAuthSpa]: sending authExpired to parent");
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
- const platform = platformKey !== null && platformKey !== void 0 ? platformKey : getPlatformKey(redirectPath);
1280
- const redirectToUrl = window.location.origin;
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
- if (logout) {
1287
- authRedirectUrl += "&logout=1";
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
- if ((isNativeApp === null || isNativeApp === void 0 ? void 0 : isNativeApp()) && scheme) {
1290
- authRedirectUrl += `&scheme=${scheme}`;
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
- // Small delay for any pending operations
1293
- await new Promise((resolve) => setTimeout(resolve, 100));
1294
- if (isNativeApp === null || isNativeApp === void 0 ? void 0 : isNativeApp()) {
1295
- // On native apps (e.g., Tauri), pass preserved token and navigate directly
1296
- if (preservedToken && preserveTokenKey) {
1297
- authRedirectUrl += `&token=${encodeURIComponent(preservedToken)}`;
1298
- console.log("[redirectToAuthSpa] Added preserved token to auth URL");
1299
- }
1300
- console.log("[redirectToAuthSpa] Native app, navigating to auth URL:", authRedirectUrl);
1301
- window.location.href = authRedirectUrl;
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 (authRedirectProxy) {
1304
- // Use auth redirect proxy endpoint
1305
- window.location.href = `${authRedirectProxy}?to=${encodeURIComponent(authRedirectUrl)}`;
1477
+ else if (diffInHours < 24) {
1478
+ return `${diffInHours} hrs ago`;
1306
1479
  }
1307
1480
  else {
1308
- window.location.href = authRedirectUrl;
1481
+ return `${diffInDays} ${diffInDays === 1 ? "day" : "days"} ago`;
1309
1482
  }
1310
1483
  }
1311
- /**
1312
- * Get the URL for joining a tenant (sign up flow)
1313
- * @param authUrl - Auth SPA base URL
1314
- * @param tenantKey - Tenant to join
1315
- * @param redirectUrl - URL to redirect to after joining (defaults to current URL)
1316
- * @returns The join URL
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 targetUrl = redirectUrl !== null && redirectUrl !== void 0 ? redirectUrl : window.location.href;
1324
- const joinUrl = `${authUrl}/join?tenant=${encodeURIComponent(resolvedTenant)}&redirect-to=${encodeURIComponent(targetUrl)}`;
1325
- return joinUrl;
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 targetUrl = redirectUrl !== null && redirectUrl !== void 0 ? redirectUrl : window.location.href;
1348
- const joinUrl = `${authUrl}/join?tenant=${encodeURIComponent(resolvedTenant)}&redirect-to=${encodeURIComponent(targetUrl)}`;
1349
- window.location.href = joinUrl;
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
- // Set logout timestamp cookie to trigger logout on other SPAs
1378
- setCookieForAuth(logoutTimestampCookie, Date.now().toString());
1379
- clearCookies();
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 ![alt](url) - 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