web-manager 4.3.2 → 4.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js ADDED
@@ -0,0 +1,712 @@
1
+ import Storage from './modules/storage.js';
2
+ import Utilities from './modules/utilities.js';
3
+ import * as domUtils from './modules/dom.js';
4
+ import Analytics from './modules/analytics.js';
5
+ import Auth from './modules/auth.js';
6
+ import Bindings from './modules/bindings.js';
7
+ import Firestore from './modules/firestore.js';
8
+ import Notifications from './modules/notifications.js';
9
+ import ServiceWorker from './modules/service-worker.js';
10
+ import Sentry from './modules/sentry.js';
11
+ import Usage from './modules/usage.js';
12
+
13
+ class Manager {
14
+ constructor() {
15
+ // Configuration from init()
16
+ this.config = {};
17
+
18
+ // Runtime state
19
+ this.state = {
20
+ serviceWorker: null
21
+ };
22
+
23
+ // Auth settler: resolves when Firebase auth first determines user state
24
+ this._firebaseAuthInitialized = false;
25
+ this._authReadyResolve = null;
26
+ this._authReady = new Promise((resolve) => {
27
+ this._authReadyResolve = resolve;
28
+ });
29
+
30
+ // Initialize modules
31
+ this._storage = new Storage();
32
+ this._utilities = new Utilities(this);
33
+ this._analytics = new Analytics(this);
34
+ this._auth = new Auth(this);
35
+ this._bindings = new Bindings(this);
36
+ this._firestore = new Firestore(this);
37
+ this._notifications = new Notifications(this);
38
+ this._serviceWorker = new ServiceWorker(this);
39
+ this._sentry = new Sentry(this);
40
+ this._usage = new Usage(this);
41
+ }
42
+
43
+ // Module getters
44
+ storage() {
45
+ return this._storage;
46
+ }
47
+
48
+ auth() {
49
+ return this._auth;
50
+ }
51
+
52
+ bindings() {
53
+ return this._bindings;
54
+ }
55
+
56
+ firestore() {
57
+ return this._firestore;
58
+ }
59
+
60
+ notifications() {
61
+ return this._notifications;
62
+ }
63
+
64
+ serviceWorker() {
65
+ return this._serviceWorker;
66
+ }
67
+
68
+ sentry() {
69
+ return this._sentry;
70
+ }
71
+
72
+ usage() {
73
+ return this._usage;
74
+ }
75
+
76
+ analytics() {
77
+ return this._analytics;
78
+ }
79
+
80
+ // DOM utilities
81
+ dom() {
82
+ return domUtils;
83
+ }
84
+
85
+ utilities() {
86
+ return this._utilities;
87
+ }
88
+
89
+ // Initialize the manager
90
+ async initialize(configuration) {
91
+ try {
92
+ // Store configuration as-is
93
+ this.config = this._processConfiguration(configuration);
94
+
95
+ // Set platform and runtime on HTML element
96
+ this._setHtmlDataAttributes();
97
+
98
+ // Initialize Firebase if a config blob is present (presence-driven — matches BEM
99
+ // convention). Reads flat `firebaseConfig` (BEM/BXM/EM canonical shape) and falls
100
+ // back to nested `firebase.app.config` (UJM's current `_config.yml` shape).
101
+ // Once UJM migrates to the flat shape this fallback can be dropped.
102
+ if (this._resolveFirebaseConfig()) {
103
+ await this._initializeFirebase();
104
+ }
105
+
106
+ // Initialize Sentry if enabled
107
+ if (this.config.sentry?.enabled) {
108
+ await this._sentry.init(this.config.sentry.config);
109
+ }
110
+
111
+ // Initialize Analytics if ID and secret are provided
112
+ if (this.config.analytics?.google && this.config.analytics?.googleSecret) {
113
+ this._analytics.init({
114
+ id: this.config.analytics.google,
115
+ secret: this.config.analytics.googleSecret,
116
+ });
117
+ } else {
118
+ console.log('[Analytics] Skipped: missing analytics google ID or secret');
119
+ }
120
+
121
+ // Initialize service worker if enabled
122
+ if (this.config.serviceWorker?.enabled) {
123
+ this._serviceWorker.register({
124
+ path: this.config.serviceWorker?.config?.path
125
+ });
126
+ }
127
+
128
+ // Start version checking if enabled
129
+ if (this.config.refreshNewVersion?.enabled) {
130
+ this._startVersionCheck();
131
+ }
132
+
133
+ // Set up auth event listeners (uses event delegation, no need to wait for DOM)
134
+ this._auth.setupEventListeners();
135
+
136
+ // Set up push notifications
137
+ if (this.config.pushNotifications?.enabled) {
138
+ this._notifications.initialize(this.config.pushNotifications.config);
139
+ }
140
+
141
+ // Initialize Chatsy chat widget if enabled
142
+ if (this.config.chatsy?.enabled && this.config.chatsy?.config?.agentId) {
143
+ this._initializeChatsy();
144
+ }
145
+
146
+ // Old IE force polyfill
147
+ // await this._loadPolyfillsIfNeeded();
148
+
149
+ // Initialize usage tracking
150
+ await this._usage.initialize();
151
+
152
+ // Update bindings with config and usage data
153
+ this.bindings().update({
154
+ config: this.config,
155
+ usage: this._usage.getBindingData(),
156
+ });
157
+
158
+ return this;
159
+ } catch (error) {
160
+ console.error('Manager initialization error:', error);
161
+ throw error;
162
+ }
163
+ }
164
+
165
+ _processConfiguration(configuration) {
166
+ // Default configuration structure
167
+ const defaults = {
168
+ runtime: null, // Auto-detect if not provided (web, browser-extension, electron, node)
169
+ environment: 'production',
170
+ buildTime: Date.now(),
171
+ brand: {
172
+ id: 'brand',
173
+ name: 'Brand',
174
+ description: '',
175
+ type: 'Organization',
176
+ images: {
177
+ brandmark: '',
178
+ wordmark: '',
179
+ combomark: ''
180
+ },
181
+ contact: {
182
+ email: '',
183
+ phone: '',
184
+ 'slapform-form-id': ''
185
+ },
186
+ address: {}
187
+ },
188
+ auth: {
189
+ enabled: true,
190
+ config: {
191
+ policy: null,
192
+ redirects: {
193
+ authenticated: '/account',
194
+ unauthenticated: '/signup'
195
+ }
196
+ }
197
+ },
198
+ firebase: {
199
+ app: {
200
+ enabled: true,
201
+ config: {}
202
+ },
203
+ appCheck: {
204
+ enabled: false,
205
+ config: {
206
+ siteKey: ''
207
+ }
208
+ }
209
+ },
210
+ cookieConsent: {
211
+ enabled: true,
212
+ config: {
213
+ palette: {
214
+ popup: {
215
+ background: '#237afc',
216
+ text: '#fff'
217
+ },
218
+ button: {
219
+ background: '#fff',
220
+ text: '#237afc'
221
+ }
222
+ },
223
+ theme: 'classic',
224
+ position: 'bottom-left',
225
+ // type: '',
226
+ // showLink: false,
227
+ content: {
228
+ message: 'We use cookies to ensure you get the best experience on our website. By continuing to use the site, you agree to our { terms }.',
229
+ dismiss: 'I Understand'
230
+ }
231
+ }
232
+ },
233
+ chatsy: {
234
+ enabled: false,
235
+ config: {
236
+ agentId: '',
237
+ settings: {
238
+ button: {
239
+ backgroundColor: '#237afc',
240
+ textColor: '#FFFFFF',
241
+ position: 'bottom-right',
242
+ type: 'round',
243
+ icon: 'default',
244
+ }
245
+ }
246
+ }
247
+ },
248
+ sentry: {
249
+ enabled: true,
250
+ config: {
251
+ dsn: '',
252
+ release: '',
253
+ replaysSessionSampleRate: 0.01,
254
+ replaysOnErrorSampleRate: 0.01
255
+ }
256
+ },
257
+ exitPopup: {
258
+ enabled: true,
259
+ config: {
260
+ timeout: 1000 * 60 * 60 * 4,
261
+ title: 'Want 15% off?',
262
+ message: 'Get 15% off your purchase of our Premium plans.',
263
+ okButton: {
264
+ text: 'Claim 15% Discount',
265
+ link: '/pricing'
266
+ }
267
+ }
268
+ },
269
+ lazyLoading: {
270
+ enabled: true,
271
+ config: {
272
+ selector: '[data-lazy]',
273
+ rootMargin: '50px 0px', // Start loading 50px before element comes into view
274
+ threshold: 0.01, // Trigger when 1% of element is visible
275
+ loadedClass: 'lazy-loaded',
276
+ loadingClass: 'lazy-loading',
277
+ errorClass: 'lazy-error'
278
+ }
279
+ },
280
+ socialSharing: {
281
+ enabled: false,
282
+ config: {
283
+ selector: '[data-social-share]',
284
+ defaultPlatforms: ['facebook', 'twitter', 'linkedin', 'pinterest', 'reddit', 'email', 'copy'],
285
+ buttonClass: '',
286
+ showLabels: false,
287
+ openInNewWindow: true,
288
+ windowWidth: 600,
289
+ windowHeight: 400,
290
+ }
291
+ },
292
+ pushNotifications: {
293
+ enabled: true,
294
+ config: {
295
+ autoRequest: 1000 * 60
296
+ }
297
+ },
298
+ env: {
299
+ FIREBASE_EMULATOR_CONNECT: false,
300
+ },
301
+ validRedirectHosts: [],
302
+ payment: {
303
+ processors: {},
304
+ products: [],
305
+ },
306
+
307
+ // Non-configurable defaults
308
+ refreshNewVersion: {
309
+ enabled: true,
310
+ config: {
311
+ interval: 1000 * 60 * 60, // Check every hour
312
+ }
313
+ },
314
+ serviceWorker: {
315
+ enabled: true,
316
+ config: {
317
+ path: '/service-worker.js'
318
+ }
319
+ },
320
+ analytics: {
321
+ google: '',
322
+ googleSecret: '',
323
+ meta: '',
324
+ tiktok: '',
325
+ },
326
+ };
327
+
328
+ // Deep merge configuration with defaults
329
+ const merged = this._deepMerge(defaults, configuration);
330
+
331
+ // Evaluate string expressions for timeout values
332
+ if (merged.exitPopup?.config?.timeout) {
333
+ merged.exitPopup.config.timeout = safeEvaluate(merged.exitPopup.config.timeout);
334
+ }
335
+
336
+ if (merged.pushNotifications?.config?.autoRequest) {
337
+ merged.pushNotifications.config.autoRequest = safeEvaluate(merged.pushNotifications.config.autoRequest);
338
+ }
339
+
340
+ if (merged.refreshNewVersion?.config?.interval) {
341
+ merged.refreshNewVersion.config.interval = safeEvaluate(merged.refreshNewVersion.config.interval);
342
+ }
343
+
344
+ // Calculate buildTimeISO from buildTime
345
+ if (merged.buildTime) {
346
+ merged.buildTimeISO = new Date(merged.buildTime).toISOString();
347
+ }
348
+
349
+ // Return merged configuration
350
+ return merged;
351
+ }
352
+
353
+ _deepMerge(target, source) {
354
+ const output = Object.assign({}, target);
355
+ if (isObject(target) && isObject(source)) {
356
+ Object.keys(source).forEach(key => {
357
+ if (isObject(source[key])) {
358
+ if (!(key in target))
359
+ Object.assign(output, { [key]: source[key] });
360
+ else
361
+ output[key] = this._deepMerge(target[key], source[key]);
362
+ } else {
363
+ Object.assign(output, { [key]: source[key] });
364
+ }
365
+ });
366
+ }
367
+ return output;
368
+
369
+ function isObject(item) {
370
+ return item && typeof item === 'object' && !Array.isArray(item);
371
+ }
372
+ }
373
+
374
+ _setHtmlDataAttributes() {
375
+ // Skip if not in browser environment
376
+ if (typeof document === 'undefined') {
377
+ return;
378
+ }
379
+
380
+ const $html = document.documentElement;
381
+
382
+ // Set platform (OS) - windows, mac, linux, ios, android, chromeos, unknown
383
+ $html.dataset.platform = this._utilities.getPlatform();
384
+
385
+ // Set browser - chrome, firefox, safari, edge, opera, brave
386
+ $html.dataset.browser = this._utilities.getBrowser();
387
+
388
+ // Set runtime - web, browser-extension, electron, node
389
+ $html.dataset.runtime = this._utilities.getRuntime();
390
+
391
+ // Set device - mobile, tablet, desktop
392
+ $html.dataset.device = this._utilities.getDevice();
393
+ }
394
+
395
+ // Resolve the Firebase web SDK config blob. Flat `firebaseConfig` first (canonical
396
+ // shape — BEM/BXM/EM), then nested `firebase.app.config` (UJM legacy yaml shape).
397
+ // Returns the blob when it has at least one own key, otherwise null.
398
+ _resolveFirebaseConfig() {
399
+ const flat = this.config.firebaseConfig;
400
+ if (flat && typeof flat === 'object' && Object.keys(flat).length > 0) {
401
+ return flat;
402
+ }
403
+ const nested = this.config.firebase?.app?.config;
404
+ if (nested && typeof nested === 'object' && Object.keys(nested).length > 0) {
405
+ return nested;
406
+ }
407
+ return null;
408
+ }
409
+
410
+ async _initializeFirebase() {
411
+ const firebaseConfig = this._resolveFirebaseConfig();
412
+
413
+ // Dynamically import Firebase v12
414
+ const { initializeApp } = await import('firebase/app');
415
+ const { getAuth, onAuthStateChanged } = await import('firebase/auth');
416
+ const { initializeFirestore } = await import('firebase/firestore');
417
+ const { getMessaging } = await import('firebase/messaging');
418
+
419
+ // If we're in devmode, set the firebase config authDomain to the current host
420
+ // if (this.isDevelopment() && firebaseConfig) {
421
+ // firebaseConfig.authDomain = window.location.hostname;
422
+ // }
423
+
424
+ // Initialize Firebase. Re-init guards: if there's already a [DEFAULT] app
425
+ // (live reload, re-init in tests), get the existing one rather than throwing
426
+ // `app/duplicate-app`.
427
+ const { getApp, getApps } = await import('firebase/app');
428
+ const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig);
429
+
430
+ // Store Firebase references
431
+ this._firebaseApp = app;
432
+ this._firebaseAuth = getAuth(app);
433
+ this._firebaseFirestore = initializeFirestore(app, {});
434
+
435
+ // Only initialize messaging if service workers are supported
436
+ if ('serviceWorker' in navigator) {
437
+ this._firebaseMessaging = getMessaging(app);
438
+ } else {
439
+ console.warn('Service workers not available - Firebase Messaging disabled');
440
+ this._firebaseMessaging = null;
441
+ }
442
+
443
+ // Initialize Firebase App Check if enabled
444
+ if (this.config.firebase.appCheck?.enabled) {
445
+ const { initializeAppCheck, ReCaptchaEnterpriseProvider } = await import('firebase/app-check');
446
+ const siteKey = this.config.firebase.appCheck.config.siteKey;
447
+
448
+ if (siteKey) {
449
+ initializeAppCheck(app, {
450
+ provider: new ReCaptchaEnterpriseProvider(siteKey),
451
+ isTokenAutoRefreshEnabled: true
452
+ });
453
+ }
454
+ }
455
+
456
+ // Setup auth state listener
457
+ onAuthStateChanged(this._firebaseAuth, (user) => {
458
+ // Mark auth as initialized and resolve the settler promise on first callback
459
+ if (!this._firebaseAuthInitialized) {
460
+ this._firebaseAuthInitialized = true;
461
+ this._authReadyResolve();
462
+ }
463
+
464
+ // Let auth module handle everything including DOM updates
465
+ this._auth._handleAuthStateChange(user);
466
+
467
+ // Update Chatsy with current user
468
+ if (this._chatsy) {
469
+ const resolved = this._auth.getUser();
470
+ this._chatsy.setUser(resolved ? { id: resolved.uid, email: resolved.email, firstName: resolved.displayName, photoURL: resolved.photoURL } : null);
471
+ }
472
+ });
473
+ }
474
+
475
+ // Getters for Firebase services
476
+ get firebaseApp() { return this._firebaseApp; }
477
+ get firebaseAuth() { return this._firebaseAuth; }
478
+ get firebaseFirestore() { return this._firebaseFirestore; }
479
+ get firebaseMessaging() { return this._firebaseMessaging; }
480
+
481
+ isDevelopment() {
482
+ return this.config.environment === 'development';
483
+ }
484
+
485
+ getFunctionsUrl(environment) {
486
+ const env = environment || this.config.environment;
487
+ const projectId = this._resolveFirebaseConfig()?.projectId;
488
+
489
+ if (!projectId) {
490
+ throw new Error('Firebase project ID not configured');
491
+ }
492
+
493
+ if (env === 'development') {
494
+ return 'http://localhost:5001/' + projectId + '/us-central1';
495
+ }
496
+
497
+ return 'https://us-central1-' + projectId + '.cloudfunctions.net';
498
+ }
499
+
500
+ getApiUrl(environment, url) {
501
+ // Precedence: passed environment > query string > config.environment
502
+ const searchParams = new URLSearchParams(window.location.search);
503
+ const queryEnv = searchParams.get('_dev_apiEnvironment');
504
+ const env = environment
505
+ || queryEnv
506
+ || this.config.environment;
507
+
508
+ if (env === 'development') {
509
+ // BEM's `mgr serve` exposes the local API over HTTPS (mkcert proxy on 5002,
510
+ // since backend-manager 5.7.0) — plain http:// cannot connect to it.
511
+ return 'https://localhost:5002';
512
+ }
513
+
514
+ const apiDomain = this._resolveFirebaseConfig()?.authDomain; // Has to be this since some projects like Clockii use ITW Universal Auth
515
+ // const apiDomain = this.config.brand.url;
516
+ const baseUrl = url || (apiDomain ? `https://${apiDomain}` : window.location.origin);
517
+ const urlObj = new URL(baseUrl);
518
+
519
+ // Prepend 'api.' subdomain (universal-auth.itwcreativeworks.com -> api.universal-auth.itwcreativeworks.com)
520
+ urlObj.hostname = `api.${urlObj.hostname}`;
521
+
522
+ // Strip trailing slash
523
+ return urlObj.toString().replace(/\/$/, '');
524
+ }
525
+
526
+ isValidRedirectUrl(url) {
527
+ try {
528
+ const returnUrlObject = new URL(decodeURIComponent(url));
529
+ const currentUrlObject = new URL(window.location.href);
530
+
531
+ // Loopback returns (RFC 8252 §7.3) are valid while the SITE runs in development:
532
+ // native apps (Electron Manager) can't OS-register their custom scheme in dev, so
533
+ // their sign-in flow returns to an ephemeral 127.0.0.1 listener instead. Any port —
534
+ // the app binds it at flow start. Production sites never match this branch.
535
+ if (this.isDevelopment() && ['127.0.0.1', '[::1]', 'localhost'].includes(returnUrlObject.hostname)) {
536
+ return true;
537
+ }
538
+
539
+ return returnUrlObject.host === currentUrlObject.host
540
+ || returnUrlObject.protocol === this.config.brand?.id + ':'
541
+ || (this.config.validRedirectHosts || []).includes(returnUrlObject.host);
542
+ } catch (e) {
543
+ return false;
544
+ }
545
+ }
546
+
547
+ async _initializeChatsy() {
548
+ try {
549
+ const { default: Chatsy } = await import('chatsy');
550
+ const config = this.config.chatsy.config;
551
+
552
+ this._chatsy = new Chatsy(config.agentId, {
553
+ settings: config.settings,
554
+ });
555
+
556
+ console.log('[Chatsy] Initialized');
557
+ } catch (error) {
558
+ console.error('[Chatsy] Failed to initialize:', error);
559
+ }
560
+ }
561
+
562
+ _startVersionCheck() {
563
+ // Quit if window is not available
564
+ if (typeof window !== 'undefined') {
565
+ // Re-focus events
566
+ window.addEventListener('focus', () => {
567
+ this._checkVersion();
568
+ });
569
+
570
+ window.addEventListener('online', () => {
571
+ this._checkVersion();
572
+ });
573
+ }
574
+
575
+ // Set up interval
576
+ this._versionCheckInterval = setInterval(() => {
577
+ this._checkVersion();
578
+ }, this.config.refreshNewVersion.config.interval);
579
+ }
580
+
581
+
582
+ // async _loadPolyfillsIfNeeded() {
583
+ // // Check if polyfills are needed by testing for ES6 features
584
+ // const featuresPass = (
585
+ // typeof Symbol !== 'undefined'
586
+ // );
587
+
588
+ // // If all features are supported, no polyfills needed
589
+ // if (featuresPass) {
590
+ // return;
591
+ // }
592
+
593
+ // // Load polyfills for older browsers (especially IE)
594
+ // try {
595
+ // await domUtils.loadScript({
596
+ // src: 'https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?flags=always%2Cgated&features=default%2Ces5%2Ces6%2Ces7%2CPromise.prototype.finally%2C%7Ehtml5-elements%2ClocalStorage%2Cfetch%2CURLSearchParams',
597
+ // crossorigin: 'anonymous'
598
+ // });
599
+ // console.log('Polyfills loaded for older browser');
600
+ // } catch (error) {
601
+ // console.error('Failed to load polyfills:', error);
602
+ // // Continue initialization even if polyfills fail to load
603
+ // }
604
+ // }
605
+
606
+ async _checkVersion() {
607
+ if (this.isDevelopment()) {
608
+ /* @dev-only:start */
609
+ {
610
+ console.log('[Version] Skipping version check in development mode');
611
+ }
612
+ /* @dev-only:end */
613
+ return;
614
+ }
615
+
616
+ try {
617
+ // Try new path first, then fallback to legacy path
618
+ const paths = ['/build.json', '/@output/build/build.json'];
619
+ let data = null;
620
+ let response = null;
621
+
622
+ for (const path of paths) {
623
+ try {
624
+ response = await fetch(`${path}?cb=${Date.now()}`);
625
+ if (response.ok) {
626
+ data = await response.json();
627
+ break;
628
+ }
629
+ } catch (e) {
630
+ // Continue to next path
631
+ }
632
+ }
633
+
634
+ if (!data) {
635
+ throw new Error('Failed to fetch build.json from any path');
636
+ }
637
+
638
+ // Extract timestamp - support both new format (data.timestamp) and legacy format (data['npm-build'].timestamp)
639
+ let buildTimeLive;
640
+ if (data.timestamp) {
641
+ buildTimeLive = new Date(data.timestamp);
642
+ } else if (data?.['npm-build']?.timestamp) {
643
+ buildTimeLive = new Date(data['npm-build'].timestamp);
644
+ } else {
645
+ throw new Error('No timestamp found in build.json');
646
+ }
647
+
648
+ const buildTimeCurrent = new Date(this.config.buildTime);
649
+
650
+ // Add 1 hour to current build time to account for npm build process
651
+ buildTimeCurrent.setHours(buildTimeCurrent.getHours() + 1);
652
+
653
+ // Log version info
654
+ console.log(`[Version] Current build time: ${buildTimeCurrent.toISOString()}, Live build time: ${buildTimeLive.toISOString()}`);
655
+
656
+ // If live version is newer, reload the page
657
+ if (buildTimeCurrent >= buildTimeLive) {
658
+ return; // No update needed
659
+ }
660
+
661
+ // New version detected
662
+ console.log('[Version] New version detected, reloading page...');
663
+
664
+ // If running in a non-browser environment, warn and return
665
+ if (typeof window === 'undefined') {
666
+ console.warn('[Version] Cannot reload in non-browser environment');
667
+ return;
668
+ }
669
+
670
+ // Force page reload
671
+ window.onbeforeunload = function () {
672
+ return undefined;
673
+ };
674
+
675
+ window.location.reload(true);
676
+ } catch (error) {
677
+ console.warn('[Version] Failed version check:', error);
678
+ }
679
+ }
680
+ }
681
+
682
+ // Safely evaluate timeout string expressions
683
+ const safeEvaluate = (str) => {
684
+ if (typeof str !== 'string') return str;
685
+
686
+ // Only allow numbers, *, +, -, /, parentheses, and whitespace
687
+ if (!/^[\d\s\*\+\-\/\(\)]+$/.test(str)) {
688
+ console.warn('Invalid expression format:', str);
689
+ return str;
690
+ }
691
+
692
+ try {
693
+ // Use Function constructor instead of eval for safer evaluation
694
+ return new Function('return ' + str)();
695
+ } catch (e) {
696
+ console.warn('Failed to evaluate expression:', str, e);
697
+ return str;
698
+ }
699
+ };
700
+
701
+ // Create singleton instance
702
+ const manager = new Manager();
703
+
704
+ // Export for different environments
705
+ export default manager;
706
+ export { Manager };
707
+
708
+ // For non-ES6 environments
709
+ if (typeof module !== 'undefined' && module.exports) {
710
+ module.exports = manager;
711
+ }
712
+