@scalemule/nextjs 0.0.1

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.mjs ADDED
@@ -0,0 +1,3084 @@
1
+ import { createContext, useState, useMemo, useEffect, useCallback, useContext, useRef } from 'react';
2
+ import { jsx } from 'react/jsx-runtime';
3
+
4
+ // src/provider.tsx
5
+
6
+ // src/client.ts
7
+ var GATEWAY_URLS = {
8
+ dev: "https://api-dev.scalemule.com",
9
+ prod: "https://api.scalemule.com"
10
+ };
11
+ var SESSION_STORAGE_KEY = "scalemule_session";
12
+ var USER_ID_STORAGE_KEY = "scalemule_user_id";
13
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
14
+ function sleep(ms) {
15
+ return new Promise((resolve) => setTimeout(resolve, ms));
16
+ }
17
+ function getBackoffDelay(attempt, baseDelay = 1e3) {
18
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
19
+ const jitter = Math.random() * 0.3 * exponentialDelay;
20
+ return Math.min(exponentialDelay + jitter, 3e4);
21
+ }
22
+ function sanitizeFilename(filename) {
23
+ let sanitized = filename.replace(/[\x00-\x1f\x7f]/g, "");
24
+ sanitized = sanitized.replace(/["\\/\n\r]/g, "_").normalize("NFC").replace(/[\u200b-\u200f\ufeff\u2028\u2029]/g, "");
25
+ if (!sanitized || sanitized.trim() === "") {
26
+ sanitized = "unnamed";
27
+ }
28
+ if (sanitized.length > 200) {
29
+ const ext = sanitized.split(".").pop();
30
+ const base = sanitized.substring(0, 190);
31
+ sanitized = ext ? `${base}.${ext}` : base;
32
+ }
33
+ return sanitized.trim();
34
+ }
35
+ var RateLimitQueue = class {
36
+ constructor() {
37
+ this.queue = [];
38
+ this.processing = false;
39
+ this.rateLimitedUntil = 0;
40
+ this.requestsInWindow = 0;
41
+ this.windowStart = Date.now();
42
+ this.maxRequestsPerWindow = 100;
43
+ this.windowDurationMs = 6e4;
44
+ }
45
+ // 1 minute
46
+ /**
47
+ * Add request to queue
48
+ */
49
+ enqueue(execute, priority = 0) {
50
+ return new Promise((resolve, reject) => {
51
+ this.queue.push({
52
+ execute,
53
+ resolve,
54
+ reject,
55
+ priority
56
+ });
57
+ this.queue.sort((a, b) => b.priority - a.priority);
58
+ this.processQueue();
59
+ });
60
+ }
61
+ /**
62
+ * Process queued requests
63
+ */
64
+ async processQueue() {
65
+ if (this.processing || this.queue.length === 0) return;
66
+ this.processing = true;
67
+ while (this.queue.length > 0) {
68
+ const now = Date.now();
69
+ if (now < this.rateLimitedUntil) {
70
+ const waitTime = this.rateLimitedUntil - now;
71
+ await sleep(waitTime);
72
+ }
73
+ if (now - this.windowStart >= this.windowDurationMs) {
74
+ this.windowStart = now;
75
+ this.requestsInWindow = 0;
76
+ }
77
+ if (this.requestsInWindow >= this.maxRequestsPerWindow) {
78
+ const waitTime = this.windowDurationMs - (now - this.windowStart);
79
+ await sleep(waitTime);
80
+ this.windowStart = Date.now();
81
+ this.requestsInWindow = 0;
82
+ }
83
+ const request = this.queue.shift();
84
+ if (!request) continue;
85
+ try {
86
+ this.requestsInWindow++;
87
+ const result = await request.execute();
88
+ if (!result.success && result.error?.code === "RATE_LIMITED") {
89
+ this.queue.unshift(request);
90
+ this.rateLimitedUntil = Date.now() + 6e4;
91
+ } else {
92
+ request.resolve(result);
93
+ }
94
+ } catch (error) {
95
+ request.reject(error);
96
+ }
97
+ }
98
+ this.processing = false;
99
+ }
100
+ /**
101
+ * Update rate limit from response headers
102
+ */
103
+ updateFromHeaders(headers) {
104
+ const retryAfter = headers.get("Retry-After");
105
+ if (retryAfter) {
106
+ const seconds = parseInt(retryAfter, 10);
107
+ if (!isNaN(seconds)) {
108
+ this.rateLimitedUntil = Date.now() + seconds * 1e3;
109
+ }
110
+ }
111
+ const remaining = headers.get("X-RateLimit-Remaining");
112
+ if (remaining) {
113
+ const count = parseInt(remaining, 10);
114
+ if (!isNaN(count) && count === 0) {
115
+ const reset = headers.get("X-RateLimit-Reset");
116
+ if (reset) {
117
+ const resetTime = parseInt(reset, 10) * 1e3;
118
+ if (!isNaN(resetTime)) {
119
+ this.rateLimitedUntil = resetTime;
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
125
+ /**
126
+ * Get queue length
127
+ */
128
+ get length() {
129
+ return this.queue.length;
130
+ }
131
+ /**
132
+ * Check if rate limited
133
+ */
134
+ get isRateLimited() {
135
+ return Date.now() < this.rateLimitedUntil;
136
+ }
137
+ };
138
+ var OfflineQueue = class {
139
+ constructor(storage) {
140
+ this.queue = [];
141
+ this.storageKey = "scalemule_offline_queue";
142
+ this.isOnline = true;
143
+ this.onOnline = null;
144
+ this.storage = storage;
145
+ this.loadFromStorage();
146
+ this.setupOnlineListener();
147
+ }
148
+ /**
149
+ * Setup online/offline event listeners
150
+ */
151
+ setupOnlineListener() {
152
+ if (typeof window === "undefined") return;
153
+ this.isOnline = navigator.onLine;
154
+ window.addEventListener("online", () => {
155
+ this.isOnline = true;
156
+ if (this.onOnline) this.onOnline();
157
+ });
158
+ window.addEventListener("offline", () => {
159
+ this.isOnline = false;
160
+ });
161
+ }
162
+ /**
163
+ * Load queue from storage
164
+ */
165
+ async loadFromStorage() {
166
+ try {
167
+ const data = await this.storage.getItem(this.storageKey);
168
+ if (data) {
169
+ this.queue = JSON.parse(data);
170
+ }
171
+ } catch {
172
+ }
173
+ }
174
+ /**
175
+ * Save queue to storage
176
+ */
177
+ async saveToStorage() {
178
+ try {
179
+ await this.storage.setItem(this.storageKey, JSON.stringify(this.queue));
180
+ } catch {
181
+ }
182
+ }
183
+ /**
184
+ * Add request to offline queue
185
+ */
186
+ async add(method, path, body) {
187
+ const item = {
188
+ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
189
+ method,
190
+ path,
191
+ body: body ? JSON.stringify(body) : void 0,
192
+ timestamp: Date.now()
193
+ };
194
+ this.queue.push(item);
195
+ await this.saveToStorage();
196
+ }
197
+ /**
198
+ * Get all queued requests
199
+ */
200
+ getAll() {
201
+ return [...this.queue];
202
+ }
203
+ /**
204
+ * Remove a request from queue
205
+ */
206
+ async remove(id) {
207
+ this.queue = this.queue.filter((item) => item.id !== id);
208
+ await this.saveToStorage();
209
+ }
210
+ /**
211
+ * Clear all queued requests
212
+ */
213
+ async clear() {
214
+ this.queue = [];
215
+ await this.saveToStorage();
216
+ }
217
+ /**
218
+ * Set callback for when coming back online
219
+ */
220
+ setOnlineCallback(callback) {
221
+ this.onOnline = callback;
222
+ }
223
+ /**
224
+ * Check if currently online
225
+ */
226
+ get online() {
227
+ return this.isOnline;
228
+ }
229
+ /**
230
+ * Get queue length
231
+ */
232
+ get length() {
233
+ return this.queue.length;
234
+ }
235
+ };
236
+ function resolveGatewayUrl(config) {
237
+ if (config.gatewayUrl) {
238
+ return config.gatewayUrl;
239
+ }
240
+ const env = config.environment || "prod";
241
+ return GATEWAY_URLS[env];
242
+ }
243
+ function createDefaultStorage() {
244
+ if (typeof window !== "undefined" && window.localStorage) {
245
+ return {
246
+ getItem: (key) => localStorage.getItem(key),
247
+ setItem: (key, value) => localStorage.setItem(key, value),
248
+ removeItem: (key) => localStorage.removeItem(key)
249
+ };
250
+ }
251
+ const memoryStorage = /* @__PURE__ */ new Map();
252
+ return {
253
+ getItem: (key) => memoryStorage.get(key) ?? null,
254
+ setItem: (key, value) => {
255
+ memoryStorage.set(key, value);
256
+ },
257
+ removeItem: (key) => {
258
+ memoryStorage.delete(key);
259
+ }
260
+ };
261
+ }
262
+ var ScaleMuleClient = class {
263
+ constructor(config) {
264
+ this.applicationId = null;
265
+ this.sessionToken = null;
266
+ this.userId = null;
267
+ this.rateLimitQueue = null;
268
+ this.offlineQueue = null;
269
+ this.apiKey = config.apiKey;
270
+ this.applicationId = config.applicationId || null;
271
+ this.gatewayUrl = resolveGatewayUrl(config);
272
+ this.debug = config.debug || false;
273
+ this.storage = config.storage || createDefaultStorage();
274
+ this.enableRateLimitQueue = config.enableRateLimitQueue || false;
275
+ this.enableOfflineQueue = config.enableOfflineQueue || false;
276
+ if (this.enableRateLimitQueue) {
277
+ this.rateLimitQueue = new RateLimitQueue();
278
+ }
279
+ if (this.enableOfflineQueue) {
280
+ this.offlineQueue = new OfflineQueue(this.storage);
281
+ this.offlineQueue.setOnlineCallback(() => this.syncOfflineQueue());
282
+ }
283
+ }
284
+ /**
285
+ * Sync offline queue when coming back online
286
+ */
287
+ async syncOfflineQueue() {
288
+ if (!this.offlineQueue) return;
289
+ const items = this.offlineQueue.getAll();
290
+ if (this.debug && items.length > 0) {
291
+ console.log(`[ScaleMule] Syncing ${items.length} offline requests`);
292
+ }
293
+ for (const item of items) {
294
+ try {
295
+ await this.request(item.path, {
296
+ method: item.method,
297
+ body: item.body,
298
+ skipRetry: true
299
+ });
300
+ await this.offlineQueue.remove(item.id);
301
+ } catch (err) {
302
+ if (this.debug) {
303
+ console.error("[ScaleMule] Failed to sync offline request:", err);
304
+ }
305
+ break;
306
+ }
307
+ }
308
+ }
309
+ /**
310
+ * Check if client is online
311
+ */
312
+ isOnline() {
313
+ if (this.offlineQueue) {
314
+ return this.offlineQueue.online;
315
+ }
316
+ return typeof navigator === "undefined" || navigator.onLine;
317
+ }
318
+ /**
319
+ * Get number of pending offline requests
320
+ */
321
+ getOfflineQueueLength() {
322
+ return this.offlineQueue?.length || 0;
323
+ }
324
+ /**
325
+ * Get number of pending rate-limited requests
326
+ */
327
+ getRateLimitQueueLength() {
328
+ return this.rateLimitQueue?.length || 0;
329
+ }
330
+ /**
331
+ * Check if currently rate limited
332
+ */
333
+ isRateLimited() {
334
+ return this.rateLimitQueue?.isRateLimited || false;
335
+ }
336
+ /**
337
+ * Get the gateway URL
338
+ */
339
+ getGatewayUrl() {
340
+ return this.gatewayUrl;
341
+ }
342
+ /**
343
+ * Get the application ID (required for realtime features)
344
+ */
345
+ getApplicationId() {
346
+ return this.applicationId;
347
+ }
348
+ /**
349
+ * Initialize client by loading persisted session
350
+ */
351
+ async initialize() {
352
+ const token = await this.storage.getItem(SESSION_STORAGE_KEY);
353
+ const userId = await this.storage.getItem(USER_ID_STORAGE_KEY);
354
+ if (token) this.sessionToken = token;
355
+ if (userId) this.userId = userId;
356
+ if (this.debug) {
357
+ console.log("[ScaleMule] Initialized with session:", !!token);
358
+ }
359
+ }
360
+ /**
361
+ * Set session after login
362
+ */
363
+ async setSession(token, userId) {
364
+ this.sessionToken = token;
365
+ this.userId = userId;
366
+ await this.storage.setItem(SESSION_STORAGE_KEY, token);
367
+ await this.storage.setItem(USER_ID_STORAGE_KEY, userId);
368
+ if (this.debug) {
369
+ console.log("[ScaleMule] Session set for user:", userId);
370
+ }
371
+ }
372
+ /**
373
+ * Clear session on logout
374
+ */
375
+ async clearSession() {
376
+ this.sessionToken = null;
377
+ this.userId = null;
378
+ await this.storage.removeItem(SESSION_STORAGE_KEY);
379
+ await this.storage.removeItem(USER_ID_STORAGE_KEY);
380
+ if (this.debug) {
381
+ console.log("[ScaleMule] Session cleared");
382
+ }
383
+ }
384
+ /**
385
+ * Get current session token
386
+ */
387
+ getSessionToken() {
388
+ return this.sessionToken;
389
+ }
390
+ /**
391
+ * Get current user ID
392
+ */
393
+ getUserId() {
394
+ return this.userId;
395
+ }
396
+ /**
397
+ * Check if client has an active session
398
+ */
399
+ isAuthenticated() {
400
+ return this.sessionToken !== null && this.userId !== null;
401
+ }
402
+ /**
403
+ * Build headers for a request
404
+ */
405
+ buildHeaders(options) {
406
+ const headers = new Headers(options?.headers);
407
+ headers.set("x-api-key", this.apiKey);
408
+ if (!options?.skipAuth && this.sessionToken) {
409
+ headers.set("Authorization", `Bearer ${this.sessionToken}`);
410
+ }
411
+ if (!headers.has("Content-Type") && options?.body && typeof options.body === "string") {
412
+ headers.set("Content-Type", "application/json");
413
+ }
414
+ return headers;
415
+ }
416
+ /**
417
+ * Make an HTTP request to the ScaleMule API
418
+ */
419
+ async request(path, options = {}) {
420
+ const url = `${this.gatewayUrl}${path}`;
421
+ const headers = this.buildHeaders(options);
422
+ const maxRetries = options.skipRetry ? 0 : options.retries ?? 2;
423
+ const timeout = options.timeout || 3e4;
424
+ if (this.debug) {
425
+ console.log(`[ScaleMule] ${options.method || "GET"} ${path}`);
426
+ }
427
+ let lastError = null;
428
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
429
+ const controller = new AbortController();
430
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
431
+ try {
432
+ const response = await fetch(url, {
433
+ ...options,
434
+ headers,
435
+ signal: controller.signal
436
+ });
437
+ clearTimeout(timeoutId);
438
+ const data = await response.json();
439
+ if (!response.ok) {
440
+ const error = data.error || {
441
+ code: `HTTP_${response.status}`,
442
+ message: data.message || response.statusText
443
+ };
444
+ if (attempt < maxRetries && RETRYABLE_STATUS_CODES.has(response.status)) {
445
+ lastError = error;
446
+ const delay = getBackoffDelay(attempt);
447
+ if (this.debug) {
448
+ console.log(`[ScaleMule] Retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
449
+ }
450
+ await sleep(delay);
451
+ continue;
452
+ }
453
+ if (this.debug) {
454
+ console.error("[ScaleMule] Request failed:", error);
455
+ }
456
+ return { success: false, error };
457
+ }
458
+ return data;
459
+ } catch (err) {
460
+ clearTimeout(timeoutId);
461
+ const error = {
462
+ code: err instanceof Error && err.name === "AbortError" ? "TIMEOUT" : "NETWORK_ERROR",
463
+ message: err instanceof Error ? err.message : "Network request failed"
464
+ };
465
+ if (attempt < maxRetries) {
466
+ lastError = error;
467
+ const delay = getBackoffDelay(attempt);
468
+ if (this.debug) {
469
+ console.log(`[ScaleMule] Retrying after error in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
470
+ }
471
+ await sleep(delay);
472
+ continue;
473
+ }
474
+ if (this.debug) {
475
+ console.error("[ScaleMule] Network error:", err);
476
+ }
477
+ return { success: false, error };
478
+ }
479
+ }
480
+ return { success: false, error: lastError || { code: "UNKNOWN", message: "Request failed" } };
481
+ }
482
+ /**
483
+ * GET request
484
+ */
485
+ async get(path, options) {
486
+ return this.request(path, { ...options, method: "GET" });
487
+ }
488
+ /**
489
+ * POST request with JSON body
490
+ */
491
+ async post(path, body, options) {
492
+ return this.request(path, {
493
+ ...options,
494
+ method: "POST",
495
+ body: body ? JSON.stringify(body) : void 0
496
+ });
497
+ }
498
+ /**
499
+ * PUT request with JSON body
500
+ */
501
+ async put(path, body, options) {
502
+ return this.request(path, {
503
+ ...options,
504
+ method: "PUT",
505
+ body: body ? JSON.stringify(body) : void 0
506
+ });
507
+ }
508
+ /**
509
+ * PATCH request with JSON body
510
+ */
511
+ async patch(path, body, options) {
512
+ return this.request(path, {
513
+ ...options,
514
+ method: "PATCH",
515
+ body: body ? JSON.stringify(body) : void 0
516
+ });
517
+ }
518
+ /**
519
+ * DELETE request
520
+ */
521
+ async delete(path, options) {
522
+ return this.request(path, { ...options, method: "DELETE" });
523
+ }
524
+ /**
525
+ * Upload a file using multipart/form-data
526
+ *
527
+ * Automatically includes Authorization: Bearer header for user identity.
528
+ * Supports progress callback via XMLHttpRequest when onProgress is provided.
529
+ */
530
+ async upload(path, file, additionalFields, options) {
531
+ const sanitizedName = sanitizeFilename(file.name);
532
+ const sanitizedFile = sanitizedName !== file.name ? new File([file], sanitizedName, { type: file.type }) : file;
533
+ const formData = new FormData();
534
+ formData.append("file", sanitizedFile);
535
+ if (this.userId) {
536
+ formData.append("sm_user_id", this.userId);
537
+ }
538
+ if (additionalFields) {
539
+ for (const [key, value] of Object.entries(additionalFields)) {
540
+ formData.append(key, value);
541
+ }
542
+ }
543
+ const url = `${this.gatewayUrl}${path}`;
544
+ if (this.debug) {
545
+ console.log(`[ScaleMule] UPLOAD ${path}`);
546
+ }
547
+ if (options?.onProgress && typeof XMLHttpRequest !== "undefined") {
548
+ return this.uploadWithProgress(url, formData, options.onProgress);
549
+ }
550
+ const maxRetries = options?.retries ?? 2;
551
+ const headers = new Headers();
552
+ headers.set("x-api-key", this.apiKey);
553
+ if (this.sessionToken) {
554
+ headers.set("Authorization", `Bearer ${this.sessionToken}`);
555
+ }
556
+ let lastError = null;
557
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
558
+ try {
559
+ const retryFormData = attempt === 0 ? formData : new FormData();
560
+ if (attempt > 0) {
561
+ retryFormData.append("file", sanitizedFile);
562
+ if (this.userId) {
563
+ retryFormData.append("sm_user_id", this.userId);
564
+ }
565
+ if (additionalFields) {
566
+ for (const [key, value] of Object.entries(additionalFields)) {
567
+ retryFormData.append(key, value);
568
+ }
569
+ }
570
+ }
571
+ const response = await fetch(url, {
572
+ method: "POST",
573
+ headers,
574
+ body: retryFormData
575
+ });
576
+ const data = await response.json();
577
+ if (!response.ok) {
578
+ const error = data.error || {
579
+ code: `HTTP_${response.status}`,
580
+ message: data.message || response.statusText
581
+ };
582
+ if (attempt < maxRetries && RETRYABLE_STATUS_CODES.has(response.status)) {
583
+ lastError = error;
584
+ const delay = getBackoffDelay(attempt);
585
+ if (this.debug) {
586
+ console.log(`[ScaleMule] Upload retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
587
+ }
588
+ await sleep(delay);
589
+ continue;
590
+ }
591
+ return { success: false, error };
592
+ }
593
+ return data;
594
+ } catch (err) {
595
+ lastError = {
596
+ code: "UPLOAD_ERROR",
597
+ message: err instanceof Error ? err.message : "Upload failed"
598
+ };
599
+ if (attempt < maxRetries) {
600
+ const delay = getBackoffDelay(attempt);
601
+ if (this.debug) {
602
+ console.log(`[ScaleMule] Upload retry ${attempt + 1}/${maxRetries} after ${delay}ms (network error)`);
603
+ }
604
+ await sleep(delay);
605
+ continue;
606
+ }
607
+ }
608
+ }
609
+ return {
610
+ success: false,
611
+ error: lastError || { code: "UPLOAD_ERROR", message: "Upload failed after retries" }
612
+ };
613
+ }
614
+ /**
615
+ * Upload with progress using XMLHttpRequest (with retry)
616
+ */
617
+ async uploadWithProgress(url, formData, onProgress, maxRetries = 2) {
618
+ let lastError = null;
619
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
620
+ const result = await this.singleUploadWithProgress(url, formData, onProgress);
621
+ if (result.success) {
622
+ return result;
623
+ }
624
+ const errorCode = result.error?.code || "";
625
+ const isNetworkError = errorCode === "UPLOAD_ERROR" || errorCode === "NETWORK_ERROR";
626
+ const isRetryableHttp = errorCode.startsWith("HTTP_") && RETRYABLE_STATUS_CODES.has(parseInt(errorCode.replace("HTTP_", ""), 10));
627
+ if (attempt < maxRetries && (isNetworkError || isRetryableHttp)) {
628
+ lastError = result.error || null;
629
+ const delay = getBackoffDelay(attempt);
630
+ if (this.debug) {
631
+ console.log(`[ScaleMule] Upload retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
632
+ }
633
+ await sleep(delay);
634
+ onProgress(0);
635
+ continue;
636
+ }
637
+ return result;
638
+ }
639
+ return {
640
+ success: false,
641
+ error: lastError || { code: "UPLOAD_ERROR", message: "Upload failed after retries" }
642
+ };
643
+ }
644
+ /**
645
+ * Single upload attempt with progress using XMLHttpRequest
646
+ */
647
+ singleUploadWithProgress(url, formData, onProgress) {
648
+ return new Promise((resolve) => {
649
+ const xhr = new XMLHttpRequest();
650
+ xhr.upload.addEventListener("progress", (event) => {
651
+ if (event.lengthComputable) {
652
+ const progress = Math.round(event.loaded / event.total * 100);
653
+ onProgress(progress);
654
+ }
655
+ });
656
+ xhr.addEventListener("load", () => {
657
+ try {
658
+ const data = JSON.parse(xhr.responseText);
659
+ if (xhr.status >= 200 && xhr.status < 300) {
660
+ resolve(data);
661
+ } else {
662
+ resolve({
663
+ success: false,
664
+ error: data.error || {
665
+ code: `HTTP_${xhr.status}`,
666
+ message: data.message || "Upload failed"
667
+ }
668
+ });
669
+ }
670
+ } catch {
671
+ resolve({
672
+ success: false,
673
+ error: { code: "PARSE_ERROR", message: "Failed to parse response" }
674
+ });
675
+ }
676
+ });
677
+ xhr.addEventListener("error", () => {
678
+ resolve({
679
+ success: false,
680
+ error: { code: "UPLOAD_ERROR", message: "Upload failed" }
681
+ });
682
+ });
683
+ xhr.addEventListener("abort", () => {
684
+ resolve({
685
+ success: false,
686
+ error: { code: "UPLOAD_ABORTED", message: "Upload cancelled" }
687
+ });
688
+ });
689
+ xhr.open("POST", url);
690
+ xhr.setRequestHeader("x-api-key", this.apiKey);
691
+ if (this.sessionToken) {
692
+ xhr.setRequestHeader("Authorization", `Bearer ${this.sessionToken}`);
693
+ }
694
+ xhr.send(formData);
695
+ });
696
+ }
697
+ };
698
+ function createClient(config) {
699
+ return new ScaleMuleClient(config);
700
+ }
701
+ var USER_CACHE_KEY = "scalemule_user";
702
+ function getCachedUser() {
703
+ if (typeof window === "undefined") return null;
704
+ try {
705
+ const cached = localStorage.getItem(USER_CACHE_KEY);
706
+ return cached ? JSON.parse(cached) : null;
707
+ } catch {
708
+ return null;
709
+ }
710
+ }
711
+ function setCachedUser(user) {
712
+ if (typeof window === "undefined") return;
713
+ try {
714
+ if (user) {
715
+ localStorage.setItem(USER_CACHE_KEY, JSON.stringify(user));
716
+ } else {
717
+ localStorage.removeItem(USER_CACHE_KEY);
718
+ }
719
+ } catch {
720
+ }
721
+ }
722
+ var ScaleMuleContext = createContext(null);
723
+ function ScaleMuleProvider({
724
+ apiKey,
725
+ applicationId,
726
+ environment,
727
+ gatewayUrl,
728
+ debug,
729
+ storage,
730
+ analyticsProxyUrl,
731
+ authProxyUrl,
732
+ publishableKey,
733
+ children,
734
+ onLogin,
735
+ onLogout,
736
+ onAuthError
737
+ }) {
738
+ const [user, setUser] = useState(null);
739
+ const [initializing, setInitializing] = useState(true);
740
+ const [error, setError] = useState(null);
741
+ const client = useMemo(
742
+ () => createClient({
743
+ apiKey,
744
+ applicationId,
745
+ environment,
746
+ gatewayUrl,
747
+ debug,
748
+ storage
749
+ }),
750
+ [apiKey, applicationId, environment, gatewayUrl, debug, storage]
751
+ );
752
+ useEffect(() => {
753
+ let mounted = true;
754
+ async function initialize() {
755
+ try {
756
+ await client.initialize();
757
+ const cachedUser = getCachedUser();
758
+ if (authProxyUrl) {
759
+ if (cachedUser && mounted) {
760
+ setUser(cachedUser);
761
+ setInitializing(false);
762
+ }
763
+ try {
764
+ const response = await fetch(`${authProxyUrl}/me`, {
765
+ credentials: "include"
766
+ });
767
+ const data = await response.json();
768
+ if (mounted) {
769
+ if (data.success && data.data?.user) {
770
+ setUser(data.data.user);
771
+ setCachedUser(data.data.user);
772
+ } else {
773
+ setUser(null);
774
+ setCachedUser(null);
775
+ }
776
+ }
777
+ } catch {
778
+ if (mounted && debug) {
779
+ console.debug("[ScaleMule] Auth proxy session check failed");
780
+ }
781
+ }
782
+ } else if (client.isAuthenticated()) {
783
+ if (cachedUser && mounted) {
784
+ setUser(cachedUser);
785
+ setInitializing(false);
786
+ }
787
+ const response = await client.get("/v1/auth/me");
788
+ if (mounted) {
789
+ if (response.success && response.data) {
790
+ setUser(response.data);
791
+ setCachedUser(response.data);
792
+ } else {
793
+ setUser(null);
794
+ setCachedUser(null);
795
+ await client.clearSession();
796
+ if (response.error && onAuthError) {
797
+ onAuthError(response.error);
798
+ }
799
+ }
800
+ }
801
+ } else if (cachedUser) {
802
+ setCachedUser(null);
803
+ }
804
+ } catch (err) {
805
+ if (mounted && debug) {
806
+ console.error("[ScaleMule] Initialization error:", err);
807
+ }
808
+ } finally {
809
+ if (mounted) {
810
+ setInitializing(false);
811
+ }
812
+ }
813
+ }
814
+ initialize();
815
+ return () => {
816
+ mounted = false;
817
+ };
818
+ }, [client, debug, onAuthError, authProxyUrl]);
819
+ const handleSetUser = useCallback(
820
+ (newUser) => {
821
+ setUser(newUser);
822
+ setCachedUser(newUser);
823
+ if (newUser === null && onLogout) {
824
+ onLogout();
825
+ }
826
+ },
827
+ [onLogout]
828
+ );
829
+ const value = useMemo(
830
+ () => ({
831
+ client,
832
+ user,
833
+ setUser: handleSetUser,
834
+ initializing,
835
+ error,
836
+ setError,
837
+ analyticsProxyUrl,
838
+ authProxyUrl,
839
+ publishableKey,
840
+ gatewayUrl: gatewayUrl || (environment === "dev" ? "https://api-dev.scalemule.com" : "https://api.scalemule.com")
841
+ }),
842
+ [client, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, gatewayUrl, environment]
843
+ );
844
+ return /* @__PURE__ */ jsx(ScaleMuleContext.Provider, { value, children });
845
+ }
846
+ function useScaleMule() {
847
+ const context = useContext(ScaleMuleContext);
848
+ if (!context) {
849
+ throw new Error(
850
+ "useScaleMule must be used within a ScaleMuleProvider. Make sure to wrap your app with <ScaleMuleProvider>."
851
+ );
852
+ }
853
+ return context;
854
+ }
855
+ function useScaleMuleClient() {
856
+ const { client } = useScaleMule();
857
+ return client;
858
+ }
859
+ function getCookie(name) {
860
+ if (typeof document === "undefined") return void 0;
861
+ const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
862
+ return match ? decodeURIComponent(match[1]) : void 0;
863
+ }
864
+ async function proxyFetch(proxyUrl, path, options = {}) {
865
+ const method = options.method || "POST";
866
+ const headers = {};
867
+ if (options.body) {
868
+ headers["Content-Type"] = "application/json";
869
+ }
870
+ if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
871
+ const csrfToken = getCookie("sm_csrf");
872
+ if (csrfToken) {
873
+ headers["x-csrf-token"] = csrfToken;
874
+ }
875
+ }
876
+ const response = await fetch(`${proxyUrl}/${path}`, {
877
+ method,
878
+ headers,
879
+ body: options.body ? JSON.stringify(options.body) : void 0,
880
+ credentials: "include"
881
+ // Include cookies for session management
882
+ });
883
+ const data = await response.json();
884
+ return data;
885
+ }
886
+ function useAuth() {
887
+ const { client, user, setUser, initializing, error, setError, authProxyUrl } = useScaleMule();
888
+ const register = useCallback(
889
+ async (data) => {
890
+ setError(null);
891
+ if (authProxyUrl) {
892
+ const response2 = await proxyFetch(
893
+ authProxyUrl,
894
+ "register",
895
+ { body: data }
896
+ );
897
+ if (!response2.success || !response2.data) {
898
+ const err = response2.error || {
899
+ code: "REGISTER_FAILED",
900
+ message: "Registration failed"
901
+ };
902
+ setError(err);
903
+ throw err;
904
+ }
905
+ return response2.data.user;
906
+ }
907
+ const response = await client.post("/v1/auth/register", data);
908
+ if (!response.success || !response.data) {
909
+ const err = response.error || {
910
+ code: "REGISTER_FAILED",
911
+ message: "Registration failed"
912
+ };
913
+ setError(err);
914
+ throw err;
915
+ }
916
+ return response.data;
917
+ },
918
+ [client, setError, authProxyUrl]
919
+ );
920
+ const login = useCallback(
921
+ async (data) => {
922
+ setError(null);
923
+ if (authProxyUrl) {
924
+ const response2 = await proxyFetch(
925
+ authProxyUrl,
926
+ "login",
927
+ { body: data }
928
+ );
929
+ if (!response2.success || !response2.data) {
930
+ const err = response2.error || {
931
+ code: "LOGIN_FAILED",
932
+ message: "Login failed"
933
+ };
934
+ setError(err);
935
+ throw err;
936
+ }
937
+ if ("requires_mfa" in response2.data && response2.data.requires_mfa) {
938
+ return response2.data;
939
+ }
940
+ const loginData2 = response2.data;
941
+ const responseUser = "user" in loginData2 ? loginData2.user : null;
942
+ if (responseUser) {
943
+ setUser(responseUser);
944
+ }
945
+ return response2.data;
946
+ }
947
+ const response = await client.post("/v1/auth/login", data);
948
+ if (!response.success || !response.data) {
949
+ const err = response.error || {
950
+ code: "LOGIN_FAILED",
951
+ message: "Login failed"
952
+ };
953
+ setError(err);
954
+ throw err;
955
+ }
956
+ if ("requires_mfa" in response.data && response.data.requires_mfa) {
957
+ return response.data;
958
+ }
959
+ const loginData = response.data;
960
+ await client.setSession(loginData.session_token, loginData.user.id);
961
+ setUser(loginData.user);
962
+ return loginData;
963
+ },
964
+ [client, setUser, setError, authProxyUrl]
965
+ );
966
+ const logout = useCallback(async () => {
967
+ setError(null);
968
+ if (authProxyUrl) {
969
+ try {
970
+ await proxyFetch(authProxyUrl, "logout");
971
+ } catch {
972
+ }
973
+ setUser(null);
974
+ return;
975
+ }
976
+ const sessionToken = client.getSessionToken();
977
+ if (sessionToken) {
978
+ try {
979
+ await client.post("/v1/auth/logout", { session_token: sessionToken });
980
+ } catch {
981
+ }
982
+ }
983
+ await client.clearSession();
984
+ setUser(null);
985
+ }, [client, setUser, setError, authProxyUrl]);
986
+ const forgotPassword = useCallback(
987
+ async (email) => {
988
+ setError(null);
989
+ const response = authProxyUrl ? await proxyFetch(authProxyUrl, "forgot-password", { body: { email } }) : await client.post("/v1/auth/forgot-password", { email });
990
+ if (!response.success) {
991
+ const err = response.error || {
992
+ code: "FORGOT_PASSWORD_FAILED",
993
+ message: "Failed to send password reset email"
994
+ };
995
+ setError(err);
996
+ throw err;
997
+ }
998
+ },
999
+ [client, setError, authProxyUrl]
1000
+ );
1001
+ const resetPassword = useCallback(
1002
+ async (token, newPassword) => {
1003
+ setError(null);
1004
+ const response = authProxyUrl ? await proxyFetch(authProxyUrl, "reset-password", { body: { token, new_password: newPassword } }) : await client.post("/v1/auth/reset-password", { token, new_password: newPassword });
1005
+ if (!response.success) {
1006
+ const err = response.error || {
1007
+ code: "RESET_PASSWORD_FAILED",
1008
+ message: "Failed to reset password"
1009
+ };
1010
+ setError(err);
1011
+ throw err;
1012
+ }
1013
+ },
1014
+ [client, setError, authProxyUrl]
1015
+ );
1016
+ const verifyEmail = useCallback(
1017
+ async (token) => {
1018
+ setError(null);
1019
+ const response = authProxyUrl ? await proxyFetch(authProxyUrl, "verify-email", { body: { token } }) : await client.post("/v1/auth/verify-email", { token });
1020
+ if (!response.success) {
1021
+ const err = response.error || {
1022
+ code: "VERIFY_EMAIL_FAILED",
1023
+ message: "Failed to verify email"
1024
+ };
1025
+ setError(err);
1026
+ throw err;
1027
+ }
1028
+ if (user) {
1029
+ if (authProxyUrl) {
1030
+ const userResponse = await proxyFetch(authProxyUrl, "me", { method: "GET" });
1031
+ if (userResponse.success && userResponse.data?.user) {
1032
+ setUser(userResponse.data.user);
1033
+ }
1034
+ } else {
1035
+ const userResponse = await client.get("/v1/auth/me");
1036
+ if (userResponse.success && userResponse.data) {
1037
+ setUser(userResponse.data);
1038
+ }
1039
+ }
1040
+ }
1041
+ },
1042
+ [client, user, setUser, setError, authProxyUrl]
1043
+ );
1044
+ const resendVerification = useCallback(async () => {
1045
+ setError(null);
1046
+ const response = authProxyUrl ? await proxyFetch(authProxyUrl, "resend-verification", { body: user ? {} : void 0 }) : (() => {
1047
+ if (!user) {
1048
+ const err = {
1049
+ code: "NOT_AUTHENTICATED",
1050
+ message: "Must be logged in to resend verification"
1051
+ };
1052
+ throw err;
1053
+ }
1054
+ return client.post("/v1/auth/resend-verification");
1055
+ })();
1056
+ const result = await response;
1057
+ if (!result.success) {
1058
+ const err = result.error || {
1059
+ code: "RESEND_FAILED",
1060
+ message: "Failed to resend verification email"
1061
+ };
1062
+ setError(err);
1063
+ throw err;
1064
+ }
1065
+ }, [client, user, setError, authProxyUrl]);
1066
+ const refreshSession = useCallback(async () => {
1067
+ setError(null);
1068
+ if (authProxyUrl) {
1069
+ const response2 = await proxyFetch(
1070
+ authProxyUrl,
1071
+ "refresh"
1072
+ );
1073
+ if (!response2.success) {
1074
+ setUser(null);
1075
+ const err = response2.error || {
1076
+ code: "REFRESH_FAILED",
1077
+ message: "Session expired"
1078
+ };
1079
+ setError(err);
1080
+ throw err;
1081
+ }
1082
+ if (response2.data?.user) {
1083
+ setUser(response2.data.user);
1084
+ }
1085
+ return;
1086
+ }
1087
+ const sessionToken = client.getSessionToken();
1088
+ if (!sessionToken) {
1089
+ const err = {
1090
+ code: "NO_SESSION",
1091
+ message: "No active session to refresh"
1092
+ };
1093
+ setError(err);
1094
+ throw err;
1095
+ }
1096
+ const response = await client.post(
1097
+ "/v1/auth/refresh",
1098
+ { session_token: sessionToken }
1099
+ );
1100
+ if (!response.success || !response.data) {
1101
+ await client.clearSession();
1102
+ setUser(null);
1103
+ const err = response.error || {
1104
+ code: "REFRESH_FAILED",
1105
+ message: "Session expired"
1106
+ };
1107
+ setError(err);
1108
+ throw err;
1109
+ }
1110
+ const userId = client.getUserId();
1111
+ if (userId) {
1112
+ await client.setSession(response.data.session_token, userId);
1113
+ }
1114
+ }, [client, setUser, setError, authProxyUrl]);
1115
+ const startOAuth = useCallback(
1116
+ async (config) => {
1117
+ setError(null);
1118
+ const response = await client.post("/v1/auth/oauth/start", {
1119
+ provider: config.provider,
1120
+ redirect_url: config.redirectUrl,
1121
+ scopes: config.scopes,
1122
+ state: config.state
1123
+ });
1124
+ if (!response.success || !response.data) {
1125
+ const err = response.error || {
1126
+ code: "OAUTH_START_FAILED",
1127
+ message: "Failed to start OAuth flow"
1128
+ };
1129
+ setError(err);
1130
+ throw err;
1131
+ }
1132
+ if (typeof sessionStorage !== "undefined") {
1133
+ sessionStorage.setItem("scalemule_oauth_state", response.data.state);
1134
+ }
1135
+ return response.data;
1136
+ },
1137
+ [client, setError]
1138
+ );
1139
+ const completeOAuth = useCallback(
1140
+ async (request) => {
1141
+ setError(null);
1142
+ if (typeof sessionStorage !== "undefined") {
1143
+ const storedState = sessionStorage.getItem("scalemule_oauth_state");
1144
+ if (storedState && storedState !== request.state) {
1145
+ const err = {
1146
+ code: "OAUTH_STATE_MISMATCH",
1147
+ message: "OAuth state mismatch - possible CSRF attack"
1148
+ };
1149
+ setError(err);
1150
+ throw err;
1151
+ }
1152
+ sessionStorage.removeItem("scalemule_oauth_state");
1153
+ }
1154
+ const response = await client.post("/v1/auth/oauth/callback", request);
1155
+ if (!response.success || !response.data) {
1156
+ const err = response.error || {
1157
+ code: "OAUTH_CALLBACK_FAILED",
1158
+ message: "Failed to complete OAuth flow"
1159
+ };
1160
+ setError(err);
1161
+ throw err;
1162
+ }
1163
+ await client.setSession(response.data.session_token, response.data.user.id);
1164
+ setUser(response.data.user);
1165
+ return response.data;
1166
+ },
1167
+ [client, setUser, setError]
1168
+ );
1169
+ const getLinkedAccounts = useCallback(async () => {
1170
+ setError(null);
1171
+ const response = await client.get("/v1/auth/oauth/accounts");
1172
+ if (!response.success || !response.data) {
1173
+ const err = response.error || {
1174
+ code: "GET_ACCOUNTS_FAILED",
1175
+ message: "Failed to get linked accounts"
1176
+ };
1177
+ setError(err);
1178
+ throw err;
1179
+ }
1180
+ return response.data.accounts;
1181
+ }, [client, setError]);
1182
+ const linkAccount = useCallback(
1183
+ async (config) => {
1184
+ setError(null);
1185
+ if (!user) {
1186
+ const err = {
1187
+ code: "NOT_AUTHENTICATED",
1188
+ message: "Must be logged in to link accounts"
1189
+ };
1190
+ setError(err);
1191
+ throw err;
1192
+ }
1193
+ const response = await client.post("/v1/auth/oauth/link", {
1194
+ provider: config.provider,
1195
+ redirect_url: config.redirectUrl,
1196
+ scopes: config.scopes
1197
+ });
1198
+ if (!response.success || !response.data) {
1199
+ const err = response.error || {
1200
+ code: "LINK_ACCOUNT_FAILED",
1201
+ message: "Failed to start account linking"
1202
+ };
1203
+ setError(err);
1204
+ throw err;
1205
+ }
1206
+ if (typeof sessionStorage !== "undefined") {
1207
+ sessionStorage.setItem("scalemule_oauth_state", response.data.state);
1208
+ }
1209
+ return response.data;
1210
+ },
1211
+ [client, user, setError]
1212
+ );
1213
+ const unlinkAccount = useCallback(
1214
+ async (provider) => {
1215
+ setError(null);
1216
+ const response = await client.delete(`/v1/auth/oauth/accounts/${provider}`);
1217
+ if (!response.success) {
1218
+ const err = response.error || {
1219
+ code: "UNLINK_ACCOUNT_FAILED",
1220
+ message: "Failed to unlink account"
1221
+ };
1222
+ setError(err);
1223
+ throw err;
1224
+ }
1225
+ },
1226
+ [client, setError]
1227
+ );
1228
+ const getMFAStatus = useCallback(async () => {
1229
+ setError(null);
1230
+ const response = await client.get("/v1/auth/mfa/status");
1231
+ if (!response.success || !response.data) {
1232
+ const err = response.error || {
1233
+ code: "GET_MFA_STATUS_FAILED",
1234
+ message: "Failed to get MFA status"
1235
+ };
1236
+ setError(err);
1237
+ throw err;
1238
+ }
1239
+ return response.data;
1240
+ }, [client, setError]);
1241
+ const setupMFA = useCallback(
1242
+ async (request) => {
1243
+ setError(null);
1244
+ const response = await client.post(
1245
+ "/v1/auth/mfa/setup",
1246
+ request
1247
+ );
1248
+ if (!response.success || !response.data) {
1249
+ const err = response.error || {
1250
+ code: "MFA_SETUP_FAILED",
1251
+ message: "Failed to setup MFA"
1252
+ };
1253
+ setError(err);
1254
+ throw err;
1255
+ }
1256
+ return response.data;
1257
+ },
1258
+ [client, setError]
1259
+ );
1260
+ const verifyMFA = useCallback(
1261
+ async (request) => {
1262
+ setError(null);
1263
+ const response = await client.post("/v1/auth/mfa/verify", request);
1264
+ if (!response.success) {
1265
+ const err = response.error || {
1266
+ code: "MFA_VERIFY_FAILED",
1267
+ message: "Failed to verify MFA code"
1268
+ };
1269
+ setError(err);
1270
+ throw err;
1271
+ }
1272
+ },
1273
+ [client, setError]
1274
+ );
1275
+ const completeMFAChallenge = useCallback(
1276
+ async (challengeToken, code, method) => {
1277
+ setError(null);
1278
+ const response = await client.post("/v1/auth/mfa/challenge", {
1279
+ challenge_token: challengeToken,
1280
+ code,
1281
+ method
1282
+ });
1283
+ if (!response.success || !response.data) {
1284
+ const err = response.error || {
1285
+ code: "MFA_CHALLENGE_FAILED",
1286
+ message: "Failed to complete MFA challenge"
1287
+ };
1288
+ setError(err);
1289
+ throw err;
1290
+ }
1291
+ await client.setSession(response.data.session_token, response.data.user.id);
1292
+ setUser(response.data.user);
1293
+ return response.data;
1294
+ },
1295
+ [client, setUser, setError]
1296
+ );
1297
+ const disableMFA = useCallback(
1298
+ async (password) => {
1299
+ setError(null);
1300
+ const response = await client.post("/v1/auth/mfa/disable", { password });
1301
+ if (!response.success) {
1302
+ const err = response.error || {
1303
+ code: "MFA_DISABLE_FAILED",
1304
+ message: "Failed to disable MFA"
1305
+ };
1306
+ setError(err);
1307
+ throw err;
1308
+ }
1309
+ },
1310
+ [client, setError]
1311
+ );
1312
+ const regenerateBackupCodes = useCallback(
1313
+ async (password) => {
1314
+ setError(null);
1315
+ const response = await client.post("/v1/auth/mfa/backup-codes", {
1316
+ password
1317
+ });
1318
+ if (!response.success || !response.data) {
1319
+ const err = response.error || {
1320
+ code: "REGENERATE_CODES_FAILED",
1321
+ message: "Failed to regenerate backup codes"
1322
+ };
1323
+ setError(err);
1324
+ throw err;
1325
+ }
1326
+ return response.data.backup_codes;
1327
+ },
1328
+ [client, setError]
1329
+ );
1330
+ const sendPhoneCode = useCallback(
1331
+ async (request) => {
1332
+ setError(null);
1333
+ const response = authProxyUrl ? await proxyFetch(authProxyUrl, "phone/send-code", { body: request }) : await client.post("/v1/auth/phone/send-code", request);
1334
+ if (!response.success) {
1335
+ const err = response.error || {
1336
+ code: "SEND_CODE_FAILED",
1337
+ message: "Failed to send verification code"
1338
+ };
1339
+ setError(err);
1340
+ throw err;
1341
+ }
1342
+ },
1343
+ [client, setError, authProxyUrl]
1344
+ );
1345
+ const verifyPhone = useCallback(
1346
+ async (request) => {
1347
+ setError(null);
1348
+ const response = authProxyUrl ? await proxyFetch(authProxyUrl, "phone/verify", { body: request }) : await client.post("/v1/auth/phone/verify", request);
1349
+ if (!response.success) {
1350
+ const err = response.error || {
1351
+ code: "VERIFY_PHONE_FAILED",
1352
+ message: "Failed to verify phone number"
1353
+ };
1354
+ setError(err);
1355
+ throw err;
1356
+ }
1357
+ if (user) {
1358
+ if (authProxyUrl) {
1359
+ const userResponse = await proxyFetch(authProxyUrl, "me", { method: "GET" });
1360
+ if (userResponse.success && userResponse.data?.user) {
1361
+ setUser(userResponse.data.user);
1362
+ }
1363
+ } else {
1364
+ const userResponse = await client.get("/v1/auth/me");
1365
+ if (userResponse.success && userResponse.data) {
1366
+ setUser(userResponse.data);
1367
+ }
1368
+ }
1369
+ }
1370
+ },
1371
+ [client, user, setUser, setError, authProxyUrl]
1372
+ );
1373
+ const loginWithPhone = useCallback(
1374
+ async (request) => {
1375
+ setError(null);
1376
+ if (authProxyUrl) {
1377
+ const response2 = await proxyFetch(
1378
+ authProxyUrl,
1379
+ "phone/login",
1380
+ { body: request }
1381
+ );
1382
+ if (!response2.success || !response2.data) {
1383
+ const err = response2.error || {
1384
+ code: "PHONE_LOGIN_FAILED",
1385
+ message: "Failed to login with phone"
1386
+ };
1387
+ setError(err);
1388
+ throw err;
1389
+ }
1390
+ const loginData = response2.data;
1391
+ const responseUser = "user" in loginData ? loginData.user : null;
1392
+ if (responseUser) {
1393
+ setUser(responseUser);
1394
+ }
1395
+ return response2.data;
1396
+ }
1397
+ const response = await client.post("/v1/auth/phone/login", request);
1398
+ if (!response.success || !response.data) {
1399
+ const err = response.error || {
1400
+ code: "PHONE_LOGIN_FAILED",
1401
+ message: "Failed to login with phone"
1402
+ };
1403
+ setError(err);
1404
+ throw err;
1405
+ }
1406
+ await client.setSession(response.data.session_token, response.data.user.id);
1407
+ setUser(response.data.user);
1408
+ return response.data;
1409
+ },
1410
+ [client, setUser, setError, authProxyUrl]
1411
+ );
1412
+ return useMemo(
1413
+ () => ({
1414
+ user,
1415
+ loading: initializing,
1416
+ isAuthenticated: !!user,
1417
+ error,
1418
+ // Basic auth
1419
+ register,
1420
+ login,
1421
+ logout,
1422
+ forgotPassword,
1423
+ resetPassword,
1424
+ verifyEmail,
1425
+ resendVerification,
1426
+ refreshSession,
1427
+ // OAuth
1428
+ startOAuth,
1429
+ completeOAuth,
1430
+ getLinkedAccounts,
1431
+ linkAccount,
1432
+ unlinkAccount,
1433
+ // MFA
1434
+ getMFAStatus,
1435
+ setupMFA,
1436
+ verifyMFA,
1437
+ completeMFAChallenge,
1438
+ disableMFA,
1439
+ regenerateBackupCodes,
1440
+ // Phone auth
1441
+ sendPhoneCode,
1442
+ verifyPhone,
1443
+ loginWithPhone
1444
+ }),
1445
+ [
1446
+ user,
1447
+ initializing,
1448
+ error,
1449
+ register,
1450
+ login,
1451
+ logout,
1452
+ forgotPassword,
1453
+ resetPassword,
1454
+ verifyEmail,
1455
+ resendVerification,
1456
+ refreshSession,
1457
+ startOAuth,
1458
+ completeOAuth,
1459
+ getLinkedAccounts,
1460
+ linkAccount,
1461
+ unlinkAccount,
1462
+ getMFAStatus,
1463
+ setupMFA,
1464
+ verifyMFA,
1465
+ completeMFAChallenge,
1466
+ disableMFA,
1467
+ regenerateBackupCodes,
1468
+ sendPhoneCode,
1469
+ verifyPhone,
1470
+ loginWithPhone
1471
+ ]
1472
+ );
1473
+ }
1474
+ function useBilling() {
1475
+ const { client } = useScaleMule();
1476
+ const [loading, setLoading] = useState(false);
1477
+ const [error, setError] = useState(null);
1478
+ const createConnectedAccount = useCallback(
1479
+ async (data) => {
1480
+ setError(null);
1481
+ setLoading(true);
1482
+ try {
1483
+ const response = await client.post("/v1/billing/connected-accounts", data);
1484
+ if (!response.success || !response.data) {
1485
+ setError(response.error || null);
1486
+ return null;
1487
+ }
1488
+ return response.data;
1489
+ } finally {
1490
+ setLoading(false);
1491
+ }
1492
+ },
1493
+ [client]
1494
+ );
1495
+ const getMyConnectedAccount = useCallback(async () => {
1496
+ setError(null);
1497
+ setLoading(true);
1498
+ try {
1499
+ const response = await client.get("/v1/billing/connected-accounts/me");
1500
+ if (!response.success || !response.data) {
1501
+ setError(response.error || null);
1502
+ return null;
1503
+ }
1504
+ return response.data;
1505
+ } finally {
1506
+ setLoading(false);
1507
+ }
1508
+ }, [client]);
1509
+ const getConnectedAccount = useCallback(
1510
+ async (id) => {
1511
+ setError(null);
1512
+ setLoading(true);
1513
+ try {
1514
+ const response = await client.get(`/v1/billing/connected-accounts/${id}`);
1515
+ if (!response.success || !response.data) {
1516
+ setError(response.error || null);
1517
+ return null;
1518
+ }
1519
+ return response.data;
1520
+ } finally {
1521
+ setLoading(false);
1522
+ }
1523
+ },
1524
+ [client]
1525
+ );
1526
+ const createOnboardingLink = useCallback(
1527
+ async (id, data) => {
1528
+ setError(null);
1529
+ setLoading(true);
1530
+ try {
1531
+ const response = await client.post(
1532
+ `/v1/billing/connected-accounts/${id}/onboarding-link`,
1533
+ data
1534
+ );
1535
+ if (!response.success || !response.data) {
1536
+ setError(response.error || null);
1537
+ return null;
1538
+ }
1539
+ return response.data.url;
1540
+ } finally {
1541
+ setLoading(false);
1542
+ }
1543
+ },
1544
+ [client]
1545
+ );
1546
+ const getAccountBalance = useCallback(
1547
+ async (id) => {
1548
+ setError(null);
1549
+ setLoading(true);
1550
+ try {
1551
+ const response = await client.get(
1552
+ `/v1/billing/connected-accounts/${id}/balance`
1553
+ );
1554
+ if (!response.success || !response.data) {
1555
+ setError(response.error || null);
1556
+ return null;
1557
+ }
1558
+ return response.data;
1559
+ } finally {
1560
+ setLoading(false);
1561
+ }
1562
+ },
1563
+ [client]
1564
+ );
1565
+ const createPayment = useCallback(
1566
+ async (data) => {
1567
+ setError(null);
1568
+ setLoading(true);
1569
+ try {
1570
+ const response = await client.post("/v1/billing/payments", data);
1571
+ if (!response.success || !response.data) {
1572
+ setError(response.error || null);
1573
+ return null;
1574
+ }
1575
+ return response.data;
1576
+ } finally {
1577
+ setLoading(false);
1578
+ }
1579
+ },
1580
+ [client]
1581
+ );
1582
+ const getPayment = useCallback(
1583
+ async (id) => {
1584
+ setError(null);
1585
+ setLoading(true);
1586
+ try {
1587
+ const response = await client.get(`/v1/billing/payments/${id}`);
1588
+ if (!response.success || !response.data) {
1589
+ setError(response.error || null);
1590
+ return null;
1591
+ }
1592
+ return response.data;
1593
+ } finally {
1594
+ setLoading(false);
1595
+ }
1596
+ },
1597
+ [client]
1598
+ );
1599
+ const listPayments = useCallback(
1600
+ async (params) => {
1601
+ setError(null);
1602
+ setLoading(true);
1603
+ try {
1604
+ const query = params ? "?" + Object.entries(params).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&") : "";
1605
+ const response = await client.get(`/v1/billing/payments${query}`);
1606
+ if (!response.success || !response.data) {
1607
+ setError(response.error || null);
1608
+ return [];
1609
+ }
1610
+ return response.data;
1611
+ } finally {
1612
+ setLoading(false);
1613
+ }
1614
+ },
1615
+ [client]
1616
+ );
1617
+ const refundPayment = useCallback(
1618
+ async (id, data) => {
1619
+ setError(null);
1620
+ setLoading(true);
1621
+ try {
1622
+ const response = await client.post(`/v1/billing/payments/${id}/refund`, data);
1623
+ if (!response.success || !response.data) {
1624
+ setError(response.error || null);
1625
+ return null;
1626
+ }
1627
+ return response.data;
1628
+ } finally {
1629
+ setLoading(false);
1630
+ }
1631
+ },
1632
+ [client]
1633
+ );
1634
+ const getPayoutHistory = useCallback(
1635
+ async (accountId, params) => {
1636
+ setError(null);
1637
+ setLoading(true);
1638
+ try {
1639
+ const query = params ? "?" + Object.entries(params).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&") : "";
1640
+ const response = await client.get(
1641
+ `/v1/billing/connected-accounts/${accountId}/payouts${query}`
1642
+ );
1643
+ if (!response.success || !response.data) {
1644
+ setError(response.error || null);
1645
+ return [];
1646
+ }
1647
+ return response.data;
1648
+ } finally {
1649
+ setLoading(false);
1650
+ }
1651
+ },
1652
+ [client]
1653
+ );
1654
+ const getPayoutSchedule = useCallback(
1655
+ async (accountId) => {
1656
+ setError(null);
1657
+ setLoading(true);
1658
+ try {
1659
+ const response = await client.get(
1660
+ `/v1/billing/connected-accounts/${accountId}/payout-schedule`
1661
+ );
1662
+ if (!response.success || !response.data) {
1663
+ setError(response.error || null);
1664
+ return null;
1665
+ }
1666
+ return response.data;
1667
+ } finally {
1668
+ setLoading(false);
1669
+ }
1670
+ },
1671
+ [client]
1672
+ );
1673
+ const setPayoutSchedule = useCallback(
1674
+ async (accountId, data) => {
1675
+ setError(null);
1676
+ setLoading(true);
1677
+ try {
1678
+ const response = await client.put(
1679
+ `/v1/billing/connected-accounts/${accountId}/payout-schedule`,
1680
+ data
1681
+ );
1682
+ if (!response.success || !response.data) {
1683
+ setError(response.error || null);
1684
+ return null;
1685
+ }
1686
+ return response.data;
1687
+ } finally {
1688
+ setLoading(false);
1689
+ }
1690
+ },
1691
+ [client]
1692
+ );
1693
+ const getTransactions = useCallback(
1694
+ async (params) => {
1695
+ setError(null);
1696
+ setLoading(true);
1697
+ try {
1698
+ const query = params ? "?" + Object.entries(params).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&") : "";
1699
+ const response = await client.get(`/v1/billing/transactions${query}`);
1700
+ if (!response.success || !response.data) {
1701
+ setError(response.error || null);
1702
+ return [];
1703
+ }
1704
+ return response.data;
1705
+ } finally {
1706
+ setLoading(false);
1707
+ }
1708
+ },
1709
+ [client]
1710
+ );
1711
+ const getTransactionSummary = useCallback(
1712
+ async (params) => {
1713
+ setError(null);
1714
+ setLoading(true);
1715
+ try {
1716
+ const query = params ? "?" + Object.entries(params).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&") : "";
1717
+ const response = await client.get(
1718
+ `/v1/billing/transactions/summary${query}`
1719
+ );
1720
+ if (!response.success || !response.data) {
1721
+ setError(response.error || null);
1722
+ return null;
1723
+ }
1724
+ return response.data;
1725
+ } finally {
1726
+ setLoading(false);
1727
+ }
1728
+ },
1729
+ [client]
1730
+ );
1731
+ const createSetupSession = useCallback(
1732
+ async (data) => {
1733
+ setError(null);
1734
+ setLoading(true);
1735
+ try {
1736
+ const response = await client.post(
1737
+ "/v1/billing/setup-sessions",
1738
+ data
1739
+ );
1740
+ if (!response.success || !response.data) {
1741
+ setError(response.error || null);
1742
+ return null;
1743
+ }
1744
+ return response.data.client_secret;
1745
+ } finally {
1746
+ setLoading(false);
1747
+ }
1748
+ },
1749
+ [client]
1750
+ );
1751
+ return useMemo(
1752
+ () => ({
1753
+ loading,
1754
+ error,
1755
+ createConnectedAccount,
1756
+ getMyConnectedAccount,
1757
+ getConnectedAccount,
1758
+ createOnboardingLink,
1759
+ getAccountBalance,
1760
+ createPayment,
1761
+ getPayment,
1762
+ listPayments,
1763
+ refundPayment,
1764
+ getPayoutHistory,
1765
+ getPayoutSchedule,
1766
+ setPayoutSchedule,
1767
+ getTransactions,
1768
+ getTransactionSummary,
1769
+ createSetupSession
1770
+ }),
1771
+ [
1772
+ loading,
1773
+ error,
1774
+ createConnectedAccount,
1775
+ getMyConnectedAccount,
1776
+ getConnectedAccount,
1777
+ createOnboardingLink,
1778
+ getAccountBalance,
1779
+ createPayment,
1780
+ getPayment,
1781
+ listPayments,
1782
+ refundPayment,
1783
+ getPayoutHistory,
1784
+ getPayoutSchedule,
1785
+ setPayoutSchedule,
1786
+ getTransactions,
1787
+ getTransactionSummary,
1788
+ createSetupSession
1789
+ ]
1790
+ );
1791
+ }
1792
+ function useContent(options = {}) {
1793
+ const { autoFetch = false, initialParams } = options;
1794
+ const { client, user, setError } = useScaleMule();
1795
+ const [files, setFiles] = useState([]);
1796
+ const [loading, setLoading] = useState(false);
1797
+ const [uploadProgress, setUploadProgress] = useState(null);
1798
+ const [error, setLocalError] = useState(null);
1799
+ const list = useCallback(
1800
+ async (params) => {
1801
+ setLocalError(null);
1802
+ setLoading(true);
1803
+ try {
1804
+ const queryParams = new URLSearchParams();
1805
+ const p = params || initialParams || {};
1806
+ if (p.content_type) queryParams.set("content_type", p.content_type);
1807
+ if (p.search) queryParams.set("search", p.search);
1808
+ if (p.limit) queryParams.set("limit", p.limit.toString());
1809
+ if (p.offset) queryParams.set("offset", p.offset.toString());
1810
+ const query = queryParams.toString();
1811
+ const path = `/v1/storage/my-files${query ? `?${query}` : ""}`;
1812
+ const response = await client.get(path);
1813
+ if (!response.success || !response.data) {
1814
+ const err = response.error || {
1815
+ code: "LIST_FAILED",
1816
+ message: "Failed to list files"
1817
+ };
1818
+ setLocalError(err);
1819
+ throw err;
1820
+ }
1821
+ setFiles(response.data.files);
1822
+ return response.data;
1823
+ } finally {
1824
+ setLoading(false);
1825
+ }
1826
+ },
1827
+ [client, initialParams]
1828
+ );
1829
+ const upload = useCallback(
1830
+ async (file, options2) => {
1831
+ setLocalError(null);
1832
+ setLoading(true);
1833
+ setUploadProgress(0);
1834
+ try {
1835
+ const additionalFields = {};
1836
+ if (options2?.is_public !== void 0) {
1837
+ additionalFields.is_public = options2.is_public ? "true" : "false";
1838
+ }
1839
+ if (options2?.filename) {
1840
+ additionalFields.filename = options2.filename;
1841
+ }
1842
+ if (options2?.category) {
1843
+ additionalFields.category = options2.category;
1844
+ }
1845
+ const onProgress = (progress) => {
1846
+ setUploadProgress(progress);
1847
+ options2?.onProgress?.(progress);
1848
+ };
1849
+ const response = await client.upload(
1850
+ "/v1/storage/upload",
1851
+ file,
1852
+ additionalFields,
1853
+ { onProgress }
1854
+ );
1855
+ if (!response.success || !response.data) {
1856
+ const err = response.error || {
1857
+ code: "UPLOAD_FAILED",
1858
+ message: "Failed to upload file"
1859
+ };
1860
+ setLocalError(err);
1861
+ throw err;
1862
+ }
1863
+ await list();
1864
+ return response.data;
1865
+ } finally {
1866
+ setLoading(false);
1867
+ setUploadProgress(null);
1868
+ }
1869
+ },
1870
+ [client, list]
1871
+ );
1872
+ const remove = useCallback(
1873
+ async (fileId) => {
1874
+ setLocalError(null);
1875
+ setLoading(true);
1876
+ try {
1877
+ const response = await client.delete(`/v1/storage/files/${fileId}`);
1878
+ if (!response.success) {
1879
+ const err = response.error || {
1880
+ code: "DELETE_FAILED",
1881
+ message: "Failed to delete file"
1882
+ };
1883
+ setLocalError(err);
1884
+ throw err;
1885
+ }
1886
+ setFiles((prev) => prev.filter((f) => f.id !== fileId));
1887
+ } finally {
1888
+ setLoading(false);
1889
+ }
1890
+ },
1891
+ [client]
1892
+ );
1893
+ const get = useCallback(
1894
+ async (fileId) => {
1895
+ setLocalError(null);
1896
+ const response = await client.get(`/v1/storage/files/${fileId}/info`);
1897
+ if (!response.success || !response.data) {
1898
+ const err = response.error || {
1899
+ code: "GET_FAILED",
1900
+ message: "Failed to get file info"
1901
+ };
1902
+ setLocalError(err);
1903
+ throw err;
1904
+ }
1905
+ return response.data;
1906
+ },
1907
+ [client]
1908
+ );
1909
+ const refresh = useCallback(async () => {
1910
+ await list(initialParams);
1911
+ }, [list, initialParams]);
1912
+ const getSignedUploadUrl = useCallback(
1913
+ async (request) => {
1914
+ setLocalError(null);
1915
+ const response = await client.post("/v1/storage/signed-upload", request);
1916
+ if (!response.success || !response.data) {
1917
+ const err = response.error || {
1918
+ code: "SIGNED_URL_FAILED",
1919
+ message: "Failed to get signed upload URL"
1920
+ };
1921
+ setLocalError(err);
1922
+ throw err;
1923
+ }
1924
+ return response.data;
1925
+ },
1926
+ [client]
1927
+ );
1928
+ const uploadToSignedUrl = useCallback(
1929
+ async (signedUrl, file, headers, onProgress) => {
1930
+ setLocalError(null);
1931
+ setLoading(true);
1932
+ setUploadProgress(0);
1933
+ try {
1934
+ await new Promise((resolve, reject) => {
1935
+ const xhr = new XMLHttpRequest();
1936
+ xhr.upload.addEventListener("progress", (event) => {
1937
+ if (event.lengthComputable) {
1938
+ const progress = Math.round(event.loaded / event.total * 100);
1939
+ setUploadProgress(progress);
1940
+ onProgress?.(progress);
1941
+ }
1942
+ });
1943
+ xhr.addEventListener("load", () => {
1944
+ if (xhr.status >= 200 && xhr.status < 300) {
1945
+ resolve();
1946
+ } else {
1947
+ reject(new Error(`Upload failed with status ${xhr.status}`));
1948
+ }
1949
+ });
1950
+ xhr.addEventListener("error", () => {
1951
+ reject(new Error("Upload failed"));
1952
+ });
1953
+ xhr.addEventListener("abort", () => {
1954
+ reject(new Error("Upload cancelled"));
1955
+ });
1956
+ xhr.open("PUT", signedUrl);
1957
+ for (const [key, value] of Object.entries(headers)) {
1958
+ xhr.setRequestHeader(key, value);
1959
+ }
1960
+ xhr.send(file);
1961
+ });
1962
+ } catch (err) {
1963
+ const error2 = {
1964
+ code: "SIGNED_UPLOAD_FAILED",
1965
+ message: err instanceof Error ? err.message : "Upload failed"
1966
+ };
1967
+ setLocalError(error2);
1968
+ throw error2;
1969
+ } finally {
1970
+ setLoading(false);
1971
+ setUploadProgress(null);
1972
+ }
1973
+ },
1974
+ []
1975
+ );
1976
+ const completeSignedUpload = useCallback(
1977
+ async (fileId) => {
1978
+ setLocalError(null);
1979
+ const response = await client.post(`/v1/storage/signed-upload/${fileId}/complete`);
1980
+ if (!response.success || !response.data) {
1981
+ const err = response.error || {
1982
+ code: "COMPLETE_UPLOAD_FAILED",
1983
+ message: "Failed to complete upload"
1984
+ };
1985
+ setLocalError(err);
1986
+ throw err;
1987
+ }
1988
+ await list();
1989
+ return response.data;
1990
+ },
1991
+ [client, list]
1992
+ );
1993
+ useEffect(() => {
1994
+ if (autoFetch && user) {
1995
+ list(initialParams);
1996
+ }
1997
+ }, [autoFetch, user, list, initialParams]);
1998
+ return useMemo(
1999
+ () => ({
2000
+ files,
2001
+ loading,
2002
+ uploadProgress,
2003
+ error,
2004
+ upload,
2005
+ list,
2006
+ remove,
2007
+ get,
2008
+ refresh,
2009
+ getSignedUploadUrl,
2010
+ uploadToSignedUrl,
2011
+ completeSignedUpload
2012
+ }),
2013
+ [
2014
+ files,
2015
+ loading,
2016
+ uploadProgress,
2017
+ error,
2018
+ upload,
2019
+ list,
2020
+ remove,
2021
+ get,
2022
+ refresh,
2023
+ getSignedUploadUrl,
2024
+ uploadToSignedUrl,
2025
+ completeSignedUpload
2026
+ ]
2027
+ );
2028
+ }
2029
+ function useUser() {
2030
+ const { client, user, setUser, setError } = useScaleMule();
2031
+ const [loading, setLoading] = useState(false);
2032
+ const [localError, setLocalError] = useState(null);
2033
+ const update = useCallback(
2034
+ async (data) => {
2035
+ setLocalError(null);
2036
+ setLoading(true);
2037
+ try {
2038
+ const response = await client.patch("/v1/auth/profile", data);
2039
+ if (!response.success || !response.data) {
2040
+ const err = response.error || {
2041
+ code: "UPDATE_FAILED",
2042
+ message: "Failed to update profile"
2043
+ };
2044
+ setLocalError(err);
2045
+ throw err;
2046
+ }
2047
+ setUser(response.data);
2048
+ return response.data;
2049
+ } finally {
2050
+ setLoading(false);
2051
+ }
2052
+ },
2053
+ [client, setUser]
2054
+ );
2055
+ const changePassword = useCallback(
2056
+ async (currentPassword, newPassword) => {
2057
+ setLocalError(null);
2058
+ setLoading(true);
2059
+ try {
2060
+ const response = await client.post("/v1/auth/change-password", {
2061
+ current_password: currentPassword,
2062
+ new_password: newPassword
2063
+ });
2064
+ if (!response.success) {
2065
+ const err = response.error || {
2066
+ code: "CHANGE_PASSWORD_FAILED",
2067
+ message: "Failed to change password"
2068
+ };
2069
+ setLocalError(err);
2070
+ throw err;
2071
+ }
2072
+ } finally {
2073
+ setLoading(false);
2074
+ }
2075
+ },
2076
+ [client]
2077
+ );
2078
+ const changeEmail = useCallback(
2079
+ async (newEmail, password) => {
2080
+ setLocalError(null);
2081
+ setLoading(true);
2082
+ try {
2083
+ const response = await client.post("/v1/auth/change-email", {
2084
+ new_email: newEmail,
2085
+ password
2086
+ });
2087
+ if (!response.success) {
2088
+ const err = response.error || {
2089
+ code: "CHANGE_EMAIL_FAILED",
2090
+ message: "Failed to change email"
2091
+ };
2092
+ setLocalError(err);
2093
+ throw err;
2094
+ }
2095
+ } finally {
2096
+ setLoading(false);
2097
+ }
2098
+ },
2099
+ [client]
2100
+ );
2101
+ const deleteAccount = useCallback(
2102
+ async (password) => {
2103
+ setLocalError(null);
2104
+ setLoading(true);
2105
+ try {
2106
+ const response = await client.post("/v1/auth/delete-account", {
2107
+ password
2108
+ });
2109
+ if (!response.success) {
2110
+ const err = response.error || {
2111
+ code: "DELETE_ACCOUNT_FAILED",
2112
+ message: "Failed to delete account"
2113
+ };
2114
+ setLocalError(err);
2115
+ throw err;
2116
+ }
2117
+ await client.clearSession();
2118
+ setUser(null);
2119
+ } finally {
2120
+ setLoading(false);
2121
+ }
2122
+ },
2123
+ [client, setUser]
2124
+ );
2125
+ const exportData = useCallback(async () => {
2126
+ setLocalError(null);
2127
+ setLoading(true);
2128
+ try {
2129
+ const response = await client.post(
2130
+ "/v1/auth/export-data"
2131
+ );
2132
+ if (!response.success || !response.data) {
2133
+ const err = response.error || {
2134
+ code: "EXPORT_FAILED",
2135
+ message: "Failed to export data"
2136
+ };
2137
+ setLocalError(err);
2138
+ throw err;
2139
+ }
2140
+ return response.data;
2141
+ } finally {
2142
+ setLoading(false);
2143
+ }
2144
+ }, [client]);
2145
+ return useMemo(
2146
+ () => ({
2147
+ profile: user,
2148
+ loading,
2149
+ error: localError,
2150
+ update,
2151
+ changePassword,
2152
+ changeEmail,
2153
+ deleteAccount,
2154
+ exportData
2155
+ }),
2156
+ [user, loading, localError, update, changePassword, changeEmail, deleteAccount, exportData]
2157
+ );
2158
+ }
2159
+ function useRealtime(options = {}) {
2160
+ const {
2161
+ autoConnect = true,
2162
+ events,
2163
+ autoReconnect = true,
2164
+ maxReconnectAttempts = 5,
2165
+ reconnectDelay = 1e3
2166
+ } = options;
2167
+ const { client, user, setUser } = useScaleMule();
2168
+ const [status, setStatus] = useState("disconnected");
2169
+ const [error, setError] = useState(null);
2170
+ const [lastMessage, setLastMessage] = useState(null);
2171
+ const wsRef = useRef(null);
2172
+ const reconnectAttempts = useRef(0);
2173
+ const reconnectTimeout = useRef(null);
2174
+ const subscribersRef = useRef(/* @__PURE__ */ new Map());
2175
+ const getWebSocketUrl = useCallback(() => {
2176
+ const gatewayUrl = client.getGatewayUrl();
2177
+ const wsUrl = gatewayUrl.replace(/^https?:\/\//, "wss://").replace(/^http:\/\//, "ws://");
2178
+ return `${wsUrl}/v1/realtime`;
2179
+ }, [client]);
2180
+ const handleMessage = useCallback(
2181
+ (event) => {
2182
+ try {
2183
+ const message = JSON.parse(event.data);
2184
+ setLastMessage(message);
2185
+ switch (message.event) {
2186
+ case "user.updated":
2187
+ if (user && message.data.id === user.id) {
2188
+ setUser(message.data);
2189
+ }
2190
+ break;
2191
+ case "session.expired":
2192
+ client.clearSession();
2193
+ setUser(null);
2194
+ break;
2195
+ }
2196
+ const subscribers = subscribersRef.current.get(message.event);
2197
+ if (subscribers) {
2198
+ subscribers.forEach((callback) => callback(message.data));
2199
+ }
2200
+ const wildcardSubscribers = subscribersRef.current.get("*");
2201
+ if (wildcardSubscribers) {
2202
+ wildcardSubscribers.forEach((callback) => callback(message));
2203
+ }
2204
+ } catch (err) {
2205
+ console.error("[ScaleMule Realtime] Failed to parse message:", err);
2206
+ }
2207
+ },
2208
+ [client, user, setUser]
2209
+ );
2210
+ const connect = useCallback(() => {
2211
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
2212
+ return;
2213
+ }
2214
+ if (!user) {
2215
+ setError({ code: "NOT_AUTHENTICATED", message: "Must be logged in to connect" });
2216
+ return;
2217
+ }
2218
+ const applicationId = client.getApplicationId();
2219
+ if (!applicationId) {
2220
+ setError({ code: "MISSING_APP_ID", message: "applicationId is required for realtime features. Add it to your ScaleMuleProvider config." });
2221
+ return;
2222
+ }
2223
+ setStatus("connecting");
2224
+ setError(null);
2225
+ const url = getWebSocketUrl();
2226
+ try {
2227
+ const ws = new WebSocket(url);
2228
+ ws.onopen = () => {
2229
+ const sessionToken = client.getSessionToken();
2230
+ ws.send(JSON.stringify({
2231
+ type: "auth",
2232
+ token: sessionToken,
2233
+ app_id: applicationId
2234
+ }));
2235
+ };
2236
+ ws.onmessage = (event) => {
2237
+ try {
2238
+ const message = JSON.parse(event.data);
2239
+ if (message.type === "auth_success") {
2240
+ setStatus("connected");
2241
+ reconnectAttempts.current = 0;
2242
+ if (events && events.length > 0) {
2243
+ ws.send(JSON.stringify({ type: "subscribe", events }));
2244
+ }
2245
+ return;
2246
+ }
2247
+ if (message.type === "error") {
2248
+ setError({ code: "AUTH_ERROR", message: message.message || "Authentication failed" });
2249
+ setStatus("disconnected");
2250
+ ws.close(1e3);
2251
+ return;
2252
+ }
2253
+ handleMessage(event);
2254
+ } catch (err) {
2255
+ console.error("[ScaleMule Realtime] Failed to parse message:", err);
2256
+ }
2257
+ };
2258
+ ws.onerror = () => {
2259
+ setError({ code: "WEBSOCKET_ERROR", message: "Connection error" });
2260
+ };
2261
+ ws.onclose = (event) => {
2262
+ setStatus("disconnected");
2263
+ wsRef.current = null;
2264
+ if (autoReconnect && event.code !== 1e3 && reconnectAttempts.current < maxReconnectAttempts) {
2265
+ setStatus("reconnecting");
2266
+ const delay = reconnectDelay * Math.pow(2, reconnectAttempts.current);
2267
+ reconnectAttempts.current++;
2268
+ reconnectTimeout.current = setTimeout(() => {
2269
+ connect();
2270
+ }, delay);
2271
+ }
2272
+ };
2273
+ wsRef.current = ws;
2274
+ } catch (err) {
2275
+ setError({
2276
+ code: "WEBSOCKET_CONNECT_FAILED",
2277
+ message: err instanceof Error ? err.message : "Failed to connect"
2278
+ });
2279
+ setStatus("disconnected");
2280
+ }
2281
+ }, [user, client, getWebSocketUrl, events, handleMessage, autoReconnect, maxReconnectAttempts, reconnectDelay]);
2282
+ const disconnect = useCallback(() => {
2283
+ if (reconnectTimeout.current) {
2284
+ clearTimeout(reconnectTimeout.current);
2285
+ reconnectTimeout.current = null;
2286
+ }
2287
+ if (wsRef.current) {
2288
+ wsRef.current.close(1e3, "Client disconnect");
2289
+ wsRef.current = null;
2290
+ }
2291
+ setStatus("disconnected");
2292
+ reconnectAttempts.current = 0;
2293
+ }, []);
2294
+ const subscribe = useCallback(
2295
+ (event, callback) => {
2296
+ if (!subscribersRef.current.has(event)) {
2297
+ subscribersRef.current.set(event, /* @__PURE__ */ new Set());
2298
+ }
2299
+ const typedCallback = callback;
2300
+ subscribersRef.current.get(event).add(typedCallback);
2301
+ return () => {
2302
+ const subscribers = subscribersRef.current.get(event);
2303
+ if (subscribers) {
2304
+ subscribers.delete(typedCallback);
2305
+ if (subscribers.size === 0) {
2306
+ subscribersRef.current.delete(event);
2307
+ }
2308
+ }
2309
+ };
2310
+ },
2311
+ []
2312
+ );
2313
+ const send = useCallback((event, data) => {
2314
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
2315
+ wsRef.current.send(JSON.stringify({ event, data }));
2316
+ } else {
2317
+ console.warn("[ScaleMule Realtime] Cannot send - not connected");
2318
+ }
2319
+ }, []);
2320
+ useEffect(() => {
2321
+ if (autoConnect && user) {
2322
+ connect();
2323
+ }
2324
+ return () => {
2325
+ disconnect();
2326
+ };
2327
+ }, [autoConnect, user, connect, disconnect]);
2328
+ return useMemo(
2329
+ () => ({
2330
+ status,
2331
+ error,
2332
+ connect,
2333
+ disconnect,
2334
+ subscribe,
2335
+ send,
2336
+ lastMessage
2337
+ }),
2338
+ [status, error, connect, disconnect, subscribe, send, lastMessage]
2339
+ );
2340
+ }
2341
+ function generateUUID() {
2342
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
2343
+ const r = Math.random() * 16 | 0;
2344
+ const v = c === "x" ? r : r & 3 | 8;
2345
+ return v.toString(16);
2346
+ });
2347
+ }
2348
+ var SESSION_START_KEY = "sm_session_start";
2349
+ var SESSION_REFERRER_KEY = "sm_session_referrer";
2350
+ function getStorageItem(storage, key) {
2351
+ if (!storage) return null;
2352
+ try {
2353
+ return storage.getItem(key);
2354
+ } catch {
2355
+ return null;
2356
+ }
2357
+ }
2358
+ function setStorageItem(storage, key, value) {
2359
+ if (!storage) return;
2360
+ try {
2361
+ storage.setItem(key, value);
2362
+ } catch {
2363
+ }
2364
+ }
2365
+ function getOrCreateIds(sessionStorageKey, anonymousStorageKey) {
2366
+ if (typeof window === "undefined") {
2367
+ return {
2368
+ sessionId: null,
2369
+ anonymousId: null,
2370
+ sessionStart: Date.now()
2371
+ };
2372
+ }
2373
+ const storage = typeof sessionStorage !== "undefined" ? sessionStorage : void 0;
2374
+ const localStorage_ = typeof localStorage !== "undefined" ? localStorage : void 0;
2375
+ let sessionId = getStorageItem(storage, sessionStorageKey);
2376
+ let sessionStartStr = getStorageItem(storage, SESSION_START_KEY);
2377
+ let sessionStart;
2378
+ if (!sessionId || !sessionStartStr) {
2379
+ sessionId = generateUUID();
2380
+ sessionStart = Date.now();
2381
+ setStorageItem(storage, sessionStorageKey, sessionId);
2382
+ setStorageItem(storage, SESSION_START_KEY, sessionStart.toString());
2383
+ } else {
2384
+ sessionStart = parseInt(sessionStartStr, 10);
2385
+ }
2386
+ let anonymousId = getStorageItem(localStorage_, anonymousStorageKey);
2387
+ if (!anonymousId) {
2388
+ anonymousId = generateUUID();
2389
+ setStorageItem(localStorage_, anonymousStorageKey, anonymousId);
2390
+ }
2391
+ return { sessionId, anonymousId, sessionStart };
2392
+ }
2393
+ function parseUtmParams() {
2394
+ if (typeof window === "undefined") return null;
2395
+ const params = new URLSearchParams(window.location.search);
2396
+ const utm = {};
2397
+ const source = params.get("utm_source");
2398
+ const medium = params.get("utm_medium");
2399
+ const campaign = params.get("utm_campaign");
2400
+ const term = params.get("utm_term");
2401
+ const content = params.get("utm_content");
2402
+ if (source) utm.utm_source = source;
2403
+ if (medium) utm.utm_medium = medium;
2404
+ if (campaign) utm.utm_campaign = campaign;
2405
+ if (term) utm.utm_term = term;
2406
+ if (content) utm.utm_content = content;
2407
+ if (!utm.utm_source && (params.get("gclid") || params.get("gad_source") || params.get("wbraid") || params.get("gbraid"))) {
2408
+ utm.utm_source = "google";
2409
+ utm.utm_medium = utm.utm_medium || "cpc";
2410
+ const gadCampaign = params.get("gad_campaignid");
2411
+ if (gadCampaign && !utm.utm_campaign) {
2412
+ utm.utm_campaign = gadCampaign;
2413
+ }
2414
+ }
2415
+ return Object.keys(utm).length > 0 ? utm : null;
2416
+ }
2417
+ function detectDeviceInfo() {
2418
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
2419
+ return {};
2420
+ }
2421
+ const ua = navigator.userAgent;
2422
+ const info = {};
2423
+ if (/Mobile|Android|iPhone|iPad|iPod/i.test(ua)) {
2424
+ if (/iPad|Tablet/i.test(ua)) {
2425
+ info.device_type = "tablet";
2426
+ } else {
2427
+ info.device_type = "mobile";
2428
+ }
2429
+ } else {
2430
+ info.device_type = "desktop";
2431
+ }
2432
+ if (/Windows/i.test(ua)) {
2433
+ info.os = "Windows";
2434
+ const match = ua.match(/Windows NT (\d+\.\d+)/);
2435
+ if (match) info.os_version = match[1];
2436
+ } else if (/Mac OS X/i.test(ua)) {
2437
+ info.os = "macOS";
2438
+ const match = ua.match(/Mac OS X (\d+[._]\d+[._]?\d*)/);
2439
+ if (match) info.os_version = match[1].replace(/_/g, ".");
2440
+ } else if (/Android/i.test(ua)) {
2441
+ info.os = "Android";
2442
+ const match = ua.match(/Android (\d+(?:\.\d+)*)/);
2443
+ if (match) info.os_version = match[1];
2444
+ } else if (/iOS|iPhone|iPad|iPod/i.test(ua)) {
2445
+ info.os = "iOS";
2446
+ const match = ua.match(/OS (\d+[._]\d+[._]?\d*)/);
2447
+ if (match) info.os_version = match[1].replace(/_/g, ".");
2448
+ } else if (/Linux/i.test(ua)) {
2449
+ info.os = "Linux";
2450
+ }
2451
+ if (/Chrome/i.test(ua) && !/Chromium|Edg/i.test(ua)) {
2452
+ info.browser = "Chrome";
2453
+ const match = ua.match(/Chrome\/(\d+(?:\.\d+)*)/);
2454
+ if (match) info.browser_version = match[1];
2455
+ } else if (/Safari/i.test(ua) && !/Chrome|Chromium/i.test(ua)) {
2456
+ info.browser = "Safari";
2457
+ const match = ua.match(/Version\/(\d+(?:\.\d+)*)/);
2458
+ if (match) info.browser_version = match[1];
2459
+ } else if (/Firefox/i.test(ua)) {
2460
+ info.browser = "Firefox";
2461
+ const match = ua.match(/Firefox\/(\d+(?:\.\d+)*)/);
2462
+ if (match) info.browser_version = match[1];
2463
+ } else if (/Edg/i.test(ua)) {
2464
+ info.browser = "Edge";
2465
+ const match = ua.match(/Edg\/(\d+(?:\.\d+)*)/);
2466
+ if (match) info.browser_version = match[1];
2467
+ }
2468
+ if (typeof screen !== "undefined") {
2469
+ info.screen_resolution = `${screen.width}x${screen.height}`;
2470
+ }
2471
+ if (typeof window !== "undefined") {
2472
+ info.viewport_size = `${window.innerWidth}x${window.innerHeight}`;
2473
+ }
2474
+ return info;
2475
+ }
2476
+ function useAnalytics(options = {}) {
2477
+ const {
2478
+ autoTrackPageViews = false,
2479
+ // Let users control this
2480
+ autoCaptureUtmParams,
2481
+ autoCapturUtmParams,
2482
+ autoGenerateSessionId = true,
2483
+ sessionStorageKey = "sm_session_id",
2484
+ anonymousStorageKey = "sm_anonymous_id",
2485
+ useV2 = true
2486
+ } = options;
2487
+ const shouldAutoCaptureUtmParams = autoCaptureUtmParams ?? autoCapturUtmParams ?? true;
2488
+ const { client, user, analyticsProxyUrl, publishableKey, gatewayUrl } = useScaleMule();
2489
+ const [loading, setLoading] = useState(false);
2490
+ const [error, setError] = useState(null);
2491
+ const [utmParams, setUtmParams] = useState(null);
2492
+ const sessionIdRef = useRef(null);
2493
+ const anonymousIdRef = useRef(null);
2494
+ const sessionStartRef = useRef(Date.now());
2495
+ const originalReferrerRef = useRef(null);
2496
+ const idsReadyRef = useRef(false);
2497
+ const [sessionId, setSessionId] = useState(null);
2498
+ const [anonymousId, setAnonymousId] = useState(null);
2499
+ const initialized = useRef(false);
2500
+ const landingPage = useRef(null);
2501
+ const eventQueue = useRef([]);
2502
+ useEffect(() => {
2503
+ if (initialized.current) return;
2504
+ initialized.current = true;
2505
+ if (!autoGenerateSessionId) {
2506
+ idsReadyRef.current = true;
2507
+ return;
2508
+ }
2509
+ const ids = getOrCreateIds(sessionStorageKey, anonymousStorageKey);
2510
+ sessionIdRef.current = ids.sessionId;
2511
+ anonymousIdRef.current = ids.anonymousId;
2512
+ sessionStartRef.current = ids.sessionStart;
2513
+ idsReadyRef.current = true;
2514
+ setSessionId(ids.sessionId);
2515
+ setAnonymousId(ids.anonymousId);
2516
+ if (eventQueue.current.length > 0) {
2517
+ const queue = eventQueue.current;
2518
+ eventQueue.current = [];
2519
+ setTimeout(() => {
2520
+ for (const event of queue) {
2521
+ sendEventRef.current?.(event);
2522
+ }
2523
+ }, 0);
2524
+ }
2525
+ }, [autoGenerateSessionId, sessionStorageKey, anonymousStorageKey]);
2526
+ useEffect(() => {
2527
+ if (typeof window === "undefined") return;
2528
+ if (shouldAutoCaptureUtmParams) {
2529
+ const utm = parseUtmParams();
2530
+ if (utm) setUtmParams(utm);
2531
+ }
2532
+ if (!landingPage.current) {
2533
+ landingPage.current = window.location.href;
2534
+ }
2535
+ const storage = typeof sessionStorage !== "undefined" ? sessionStorage : void 0;
2536
+ const storedReferrer = getStorageItem(storage, SESSION_REFERRER_KEY);
2537
+ if (storedReferrer) {
2538
+ originalReferrerRef.current = storedReferrer;
2539
+ } else if (document.referrer) {
2540
+ try {
2541
+ const referrerUrl = new URL(document.referrer);
2542
+ const currentUrl = new URL(window.location.href);
2543
+ if (referrerUrl.hostname !== currentUrl.hostname) {
2544
+ originalReferrerRef.current = document.referrer;
2545
+ setStorageItem(storage, SESSION_REFERRER_KEY, document.referrer);
2546
+ }
2547
+ } catch {
2548
+ }
2549
+ }
2550
+ }, [shouldAutoCaptureUtmParams]);
2551
+ const sendEventRef = useRef(null);
2552
+ const getDeviceInfo = useCallback(() => {
2553
+ return detectDeviceInfo();
2554
+ }, []);
2555
+ const buildFullEvent = useCallback(
2556
+ (event) => {
2557
+ const device = getDeviceInfo();
2558
+ const fullEvent = {
2559
+ event_name: event.event_name,
2560
+ event_category: event.event_category,
2561
+ properties: event.properties,
2562
+ // Use refs for IDs - they're always current, and this keeps the callback stable
2563
+ session_id: event.session_id || sessionIdRef.current,
2564
+ anonymous_id: event.anonymous_id || anonymousIdRef.current,
2565
+ user_id: event.user_id || user?.id,
2566
+ client_timestamp: event.client_timestamp || (/* @__PURE__ */ new Date()).toISOString(),
2567
+ // Device info
2568
+ device_type: device.device_type,
2569
+ device_brand: device.device_brand,
2570
+ device_model: device.device_model,
2571
+ os: device.os,
2572
+ os_version: device.os_version,
2573
+ browser: device.browser,
2574
+ browser_version: device.browser_version,
2575
+ screen_resolution: device.screen_resolution,
2576
+ viewport_size: device.viewport_size,
2577
+ // UTM params
2578
+ ...utmParams || {},
2579
+ // Landing page (first page visited)
2580
+ landing_page: landingPage.current,
2581
+ // Session duration in seconds
2582
+ session_duration_seconds: Math.floor((Date.now() - sessionStartRef.current) / 1e3)
2583
+ };
2584
+ if (typeof window !== "undefined") {
2585
+ fullEvent.page_url = window.location.href;
2586
+ fullEvent.page_title = document.title;
2587
+ fullEvent.referrer = originalReferrerRef.current || document.referrer || void 0;
2588
+ }
2589
+ return fullEvent;
2590
+ },
2591
+ // Note: sessionId/anonymousId removed - we use refs to keep this stable
2592
+ [user, utmParams, getDeviceInfo]
2593
+ );
2594
+ const sendEvent = useCallback(
2595
+ async (event) => {
2596
+ const fullEvent = buildFullEvent(event);
2597
+ if (analyticsProxyUrl) {
2598
+ fetch(analyticsProxyUrl, {
2599
+ method: "POST",
2600
+ headers: { "Content-Type": "application/json" },
2601
+ body: JSON.stringify(fullEvent)
2602
+ }).catch((err) => {
2603
+ console.debug("[ScaleMule Analytics] Proxy tracking failed:", err);
2604
+ });
2605
+ return { tracked: 1, session_id: sessionIdRef.current || void 0 };
2606
+ }
2607
+ if (publishableKey && gatewayUrl) {
2608
+ const endpoint2 = useV2 ? "/v1/analytics/v2/events" : "/v1/analytics/events";
2609
+ fetch(`${gatewayUrl}${endpoint2}`, {
2610
+ method: "POST",
2611
+ headers: {
2612
+ "Content-Type": "application/json",
2613
+ "x-api-key": publishableKey
2614
+ },
2615
+ body: JSON.stringify(fullEvent)
2616
+ }).catch((err) => {
2617
+ console.debug("[ScaleMule Analytics] Direct tracking failed:", err);
2618
+ });
2619
+ return { tracked: 1, session_id: sessionIdRef.current || void 0 };
2620
+ }
2621
+ const endpoint = useV2 ? "/v1/analytics/v2/events" : "/v1/analytics/events";
2622
+ const response = await client.post(endpoint, fullEvent);
2623
+ if (!response.success) {
2624
+ const err = response.error || {
2625
+ code: "TRACK_FAILED",
2626
+ message: "Failed to track event"
2627
+ };
2628
+ throw err;
2629
+ }
2630
+ return response.data || { tracked: 1, session_id: sessionIdRef.current || void 0 };
2631
+ },
2632
+ // Note: sessionId removed - we use ref to keep this stable
2633
+ [client, buildFullEvent, useV2, analyticsProxyUrl, publishableKey, gatewayUrl]
2634
+ );
2635
+ sendEventRef.current = sendEvent;
2636
+ const trackEvent = useCallback(
2637
+ async (event) => {
2638
+ setError(null);
2639
+ setLoading(true);
2640
+ try {
2641
+ if (!idsReadyRef.current) {
2642
+ eventQueue.current.push(event);
2643
+ setLoading(false);
2644
+ return { tracked: 0, session_id: void 0 };
2645
+ }
2646
+ return await sendEvent(event);
2647
+ } catch (err) {
2648
+ if (err && typeof err === "object" && "code" in err) {
2649
+ setError(err);
2650
+ }
2651
+ throw err;
2652
+ } finally {
2653
+ setLoading(false);
2654
+ }
2655
+ },
2656
+ // Note: idsReady removed - we use ref to keep callback stable
2657
+ [sendEvent]
2658
+ );
2659
+ const trackPageView = useCallback(
2660
+ async (data) => {
2661
+ const pageEvent = {
2662
+ event_name: "page_viewed",
2663
+ event_category: "navigation",
2664
+ properties: {
2665
+ ...data?.properties || {},
2666
+ page_url: data?.page_url || (typeof window !== "undefined" ? window.location.href : void 0),
2667
+ page_title: data?.page_title || (typeof document !== "undefined" ? document.title : void 0),
2668
+ referrer: data?.referrer || (typeof document !== "undefined" ? document.referrer : void 0)
2669
+ }
2670
+ };
2671
+ return trackEvent(pageEvent);
2672
+ },
2673
+ [trackEvent]
2674
+ );
2675
+ const trackBatch = useCallback(
2676
+ async (events) => {
2677
+ setError(null);
2678
+ setLoading(true);
2679
+ try {
2680
+ const fullEvents = events.map((event) => buildFullEvent(event));
2681
+ if (analyticsProxyUrl) {
2682
+ for (const event of fullEvents) {
2683
+ fetch(analyticsProxyUrl, {
2684
+ method: "POST",
2685
+ headers: { "Content-Type": "application/json" },
2686
+ body: JSON.stringify(event)
2687
+ }).catch((err) => {
2688
+ console.debug("[ScaleMule Analytics] Proxy batch tracking failed:", err);
2689
+ });
2690
+ }
2691
+ setLoading(false);
2692
+ return { tracked: events.length, session_id: sessionIdRef.current || void 0 };
2693
+ }
2694
+ if (publishableKey && gatewayUrl) {
2695
+ const endpoint2 = useV2 ? "/v1/analytics/v2/events/batch" : "/v1/analytics/events/batch";
2696
+ fetch(`${gatewayUrl}${endpoint2}`, {
2697
+ method: "POST",
2698
+ headers: {
2699
+ "Content-Type": "application/json",
2700
+ "x-api-key": publishableKey
2701
+ },
2702
+ body: JSON.stringify({ events: fullEvents })
2703
+ }).catch((err) => {
2704
+ console.debug("[ScaleMule Analytics] Direct batch tracking failed:", err);
2705
+ });
2706
+ setLoading(false);
2707
+ return { tracked: events.length, session_id: sessionIdRef.current || void 0 };
2708
+ }
2709
+ const endpoint = useV2 ? "/v1/analytics/v2/events/batch" : "/v1/analytics/events/batch";
2710
+ const response = await client.post(endpoint, {
2711
+ events: fullEvents
2712
+ });
2713
+ if (!response.success) {
2714
+ const err = response.error || {
2715
+ code: "BATCH_TRACK_FAILED",
2716
+ message: "Failed to track events"
2717
+ };
2718
+ setError(err);
2719
+ throw err;
2720
+ }
2721
+ return response.data || { tracked: events.length, session_id: sessionIdRef.current || void 0 };
2722
+ } finally {
2723
+ setLoading(false);
2724
+ }
2725
+ },
2726
+ // Note: sessionId removed - we use ref to keep callback stable
2727
+ [client, buildFullEvent, useV2, analyticsProxyUrl, publishableKey, gatewayUrl]
2728
+ );
2729
+ const identify = useCallback(
2730
+ async (userId, traits) => {
2731
+ await trackEvent({
2732
+ event_name: "user_identified",
2733
+ event_category: "identity",
2734
+ user_id: userId,
2735
+ properties: {
2736
+ ...traits || {},
2737
+ previous_anonymous_id: anonymousIdRef.current
2738
+ }
2739
+ });
2740
+ },
2741
+ // Note: anonymousId removed - we use ref
2742
+ [trackEvent]
2743
+ );
2744
+ const reset = useCallback(() => {
2745
+ const newSessionId = generateUUID();
2746
+ const newSessionStart = Date.now();
2747
+ sessionIdRef.current = newSessionId;
2748
+ sessionStartRef.current = newSessionStart;
2749
+ setSessionId(newSessionId);
2750
+ if (typeof sessionStorage !== "undefined") {
2751
+ sessionStorage.setItem(sessionStorageKey, newSessionId);
2752
+ sessionStorage.setItem(SESSION_START_KEY, newSessionStart.toString());
2753
+ sessionStorage.removeItem(SESSION_REFERRER_KEY);
2754
+ }
2755
+ originalReferrerRef.current = null;
2756
+ setUtmParams(null);
2757
+ }, [sessionStorageKey]);
2758
+ const setUtmParamsManual = useCallback((params) => {
2759
+ setUtmParams(params);
2760
+ }, []);
2761
+ return useMemo(
2762
+ () => ({
2763
+ loading,
2764
+ error,
2765
+ sessionId,
2766
+ anonymousId,
2767
+ utmParams,
2768
+ trackEvent,
2769
+ trackPageView,
2770
+ trackBatch,
2771
+ identify,
2772
+ reset,
2773
+ setUtmParams: setUtmParamsManual,
2774
+ getDeviceInfo
2775
+ }),
2776
+ [
2777
+ loading,
2778
+ error,
2779
+ sessionId,
2780
+ anonymousId,
2781
+ utmParams,
2782
+ trackEvent,
2783
+ trackPageView,
2784
+ trackBatch,
2785
+ identify,
2786
+ reset,
2787
+ setUtmParamsManual,
2788
+ getDeviceInfo
2789
+ ]
2790
+ );
2791
+ }
2792
+
2793
+ // src/validation.ts
2794
+ var phoneCountries = [
2795
+ { code: "US", name: "United States", dialCode: "+1" },
2796
+ { code: "CA", name: "Canada", dialCode: "+1" },
2797
+ { code: "GB", name: "United Kingdom", dialCode: "+44" },
2798
+ { code: "AU", name: "Australia", dialCode: "+61" },
2799
+ { code: "DE", name: "Germany", dialCode: "+49" },
2800
+ { code: "FR", name: "France", dialCode: "+33" },
2801
+ { code: "IT", name: "Italy", dialCode: "+39" },
2802
+ { code: "ES", name: "Spain", dialCode: "+34" },
2803
+ { code: "NL", name: "Netherlands", dialCode: "+31" },
2804
+ { code: "SE", name: "Sweden", dialCode: "+46" },
2805
+ { code: "JP", name: "Japan", dialCode: "+81" },
2806
+ { code: "KR", name: "South Korea", dialCode: "+82" },
2807
+ { code: "CN", name: "China", dialCode: "+86" },
2808
+ { code: "SG", name: "Singapore", dialCode: "+65" },
2809
+ { code: "IN", name: "India", dialCode: "+91" },
2810
+ { code: "AE", name: "UAE", dialCode: "+971" },
2811
+ { code: "ZA", name: "South Africa", dialCode: "+27" },
2812
+ { code: "NG", name: "Nigeria", dialCode: "+234" },
2813
+ { code: "BR", name: "Brazil", dialCode: "+55" },
2814
+ { code: "MX", name: "Mexico", dialCode: "+52" },
2815
+ { code: "NZ", name: "New Zealand", dialCode: "+64" }
2816
+ ];
2817
+ function normalizePhone(input) {
2818
+ if (!input || typeof input !== "string") return "";
2819
+ const trimmed = input.trim();
2820
+ if (!trimmed) return "";
2821
+ const digits = trimmed.replace(/\D/g, "");
2822
+ if (!digits) return "";
2823
+ if (trimmed.startsWith("+")) return `+${digits}`;
2824
+ if (trimmed.startsWith("00") && digits.length > 2) return `+${digits.slice(2)}`;
2825
+ return `+${digits}`;
2826
+ }
2827
+ function composePhone(countryDialCode, localNumber) {
2828
+ const dial = normalizePhone(countryDialCode);
2829
+ if (!dial) return "";
2830
+ const localDigits = (localNumber || "").replace(/\D/g, "");
2831
+ if (!localDigits) return "";
2832
+ return `${dial}${localDigits}`;
2833
+ }
2834
+ var validators = {
2835
+ /**
2836
+ * Validate email address format.
2837
+ * Matches RFC 5322 simplified pattern used by ScaleMule backend.
2838
+ */
2839
+ email: (email) => {
2840
+ if (!email || typeof email !== "string") return false;
2841
+ if (email.length > 254) return false;
2842
+ const atIndex = email.lastIndexOf("@");
2843
+ if (atIndex === -1 || atIndex > 64) return false;
2844
+ const re = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
2845
+ return re.test(email);
2846
+ },
2847
+ /**
2848
+ * Validate password strength.
2849
+ * Returns detailed result with errors and strength indicator.
2850
+ */
2851
+ password: (password) => {
2852
+ const errors = [];
2853
+ if (!password || typeof password !== "string") {
2854
+ return { valid: false, errors: ["Password is required"], strength: "weak" };
2855
+ }
2856
+ if (password.length < 8) {
2857
+ errors.push("At least 8 characters required");
2858
+ }
2859
+ if (password.length > 128) {
2860
+ errors.push("Maximum 128 characters");
2861
+ }
2862
+ let score = 0;
2863
+ if (password.length >= 8) score++;
2864
+ if (password.length >= 12) score++;
2865
+ if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++;
2866
+ if (/\d/.test(password)) score++;
2867
+ if (/[^a-zA-Z0-9]/.test(password)) score++;
2868
+ let strength = "weak";
2869
+ if (score >= 4) strength = "strong";
2870
+ else if (score >= 3) strength = "good";
2871
+ else if (score >= 2) strength = "fair";
2872
+ return {
2873
+ valid: errors.length === 0,
2874
+ errors,
2875
+ strength
2876
+ };
2877
+ },
2878
+ /**
2879
+ * Validate phone number in E.164 format.
2880
+ * ScaleMule requires E.164 format: +[country code][number]
2881
+ */
2882
+ phone: (phone) => {
2883
+ if (!phone || typeof phone !== "string") {
2884
+ return { valid: false, formatted: null, error: "Phone number is required" };
2885
+ }
2886
+ const rawDigits = phone.trim().replace(/\D/g, "");
2887
+ const hasIntlPrefix = phone.trim().startsWith("+") || phone.trim().startsWith("00");
2888
+ if (!hasIntlPrefix && /^\d{10}$/.test(rawDigits)) {
2889
+ return {
2890
+ valid: false,
2891
+ formatted: `+1${rawDigits}`,
2892
+ error: "Add country code (e.g., +1 for US)"
2893
+ };
2894
+ }
2895
+ const cleaned = normalizePhone(phone);
2896
+ const e164Regex = /^\+[1-9]\d{1,14}$/;
2897
+ if (e164Regex.test(cleaned)) {
2898
+ return { valid: true, formatted: cleaned, error: null };
2899
+ }
2900
+ return {
2901
+ valid: false,
2902
+ formatted: null,
2903
+ error: "Use E.164 format: +[country code][number]"
2904
+ };
2905
+ },
2906
+ /**
2907
+ * Validate username format.
2908
+ * Alphanumeric with underscores, 3-30 characters.
2909
+ */
2910
+ username: (username) => {
2911
+ if (!username || typeof username !== "string") {
2912
+ return { valid: false, error: "Username is required" };
2913
+ }
2914
+ if (username.length < 3) {
2915
+ return { valid: false, error: "At least 3 characters required" };
2916
+ }
2917
+ if (username.length > 30) {
2918
+ return { valid: false, error: "Maximum 30 characters" };
2919
+ }
2920
+ if (!/^[a-zA-Z0-9_]+$/.test(username)) {
2921
+ return { valid: false, error: "Only letters, numbers, and underscores allowed" };
2922
+ }
2923
+ if (/^[_0-9]/.test(username)) {
2924
+ return { valid: false, error: "Must start with a letter" };
2925
+ }
2926
+ return { valid: true, error: null };
2927
+ },
2928
+ /**
2929
+ * Validate UUID format.
2930
+ * Accepts UUIDv1, v4, v7 formats.
2931
+ */
2932
+ uuid: (uuid) => {
2933
+ if (!uuid || typeof uuid !== "string") return false;
2934
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2935
+ return uuidRegex.test(uuid);
2936
+ },
2937
+ /**
2938
+ * Validate URL format.
2939
+ */
2940
+ url: (url) => {
2941
+ if (!url || typeof url !== "string") return false;
2942
+ try {
2943
+ const parsed = new URL(url);
2944
+ return ["http:", "https:"].includes(parsed.protocol);
2945
+ } catch {
2946
+ return false;
2947
+ }
2948
+ },
2949
+ /**
2950
+ * Validate file size against ScaleMule limits.
2951
+ * Default max is 100MB, can be customized per application.
2952
+ */
2953
+ fileSize: (bytes, maxMB = 100) => {
2954
+ if (!Number.isFinite(bytes) || bytes < 0) {
2955
+ return { valid: false, error: "Invalid file size" };
2956
+ }
2957
+ if (!Number.isFinite(maxMB) || maxMB <= 0) {
2958
+ return { valid: false, error: "Invalid max file size" };
2959
+ }
2960
+ const maxBytes = maxMB * 1024 * 1024;
2961
+ if (bytes > maxBytes) {
2962
+ return { valid: false, error: `File exceeds ${maxMB}MB limit` };
2963
+ }
2964
+ if (bytes === 0) {
2965
+ return { valid: false, error: "File is empty" };
2966
+ }
2967
+ return { valid: true, error: null };
2968
+ },
2969
+ /**
2970
+ * Validate file type against allowed MIME types.
2971
+ */
2972
+ fileType: (mimeType, allowed = ["image/jpeg", "image/png", "image/gif", "image/webp", "application/pdf"]) => {
2973
+ if (!mimeType || typeof mimeType !== "string") {
2974
+ return { valid: false, error: "Unknown file type" };
2975
+ }
2976
+ if (allowed.includes(mimeType)) {
2977
+ return { valid: true, error: null };
2978
+ }
2979
+ const category = mimeType.split("/")[0];
2980
+ if (allowed.includes(`${category}/*`)) {
2981
+ return { valid: true, error: null };
2982
+ }
2983
+ return { valid: false, error: `File type ${mimeType} not allowed` };
2984
+ },
2985
+ /**
2986
+ * Sanitize and validate a display name.
2987
+ */
2988
+ displayName: (name) => {
2989
+ if (!name || typeof name !== "string") {
2990
+ return { valid: false, sanitized: "", error: "Display name is required" };
2991
+ }
2992
+ const sanitized = name.trim().replace(/\s+/g, " ");
2993
+ if (sanitized.length < 1) {
2994
+ return { valid: false, sanitized, error: "Display name is required" };
2995
+ }
2996
+ if (sanitized.length > 100) {
2997
+ return { valid: false, sanitized: sanitized.slice(0, 100), error: "Maximum 100 characters" };
2998
+ }
2999
+ if (/[\x00-\x1F\x7F]/.test(sanitized)) {
3000
+ return { valid: false, sanitized: sanitized.replace(/[\x00-\x1F\x7F]/g, ""), error: "Invalid characters" };
3001
+ }
3002
+ return { valid: true, sanitized, error: null };
3003
+ }
3004
+ };
3005
+ function validateForm(data, rules) {
3006
+ const errors = {};
3007
+ for (const [field, validator] of Object.entries(rules)) {
3008
+ if (!validator) continue;
3009
+ const value = data[field];
3010
+ const result = validator(value);
3011
+ if (typeof result === "boolean") {
3012
+ if (!result) {
3013
+ errors[field] = "Invalid value";
3014
+ }
3015
+ } else if (!result.valid) {
3016
+ errors[field] = result.error || "Invalid value";
3017
+ }
3018
+ }
3019
+ return {
3020
+ valid: Object.keys(errors).length === 0,
3021
+ errors
3022
+ };
3023
+ }
3024
+ var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
3025
+ "password",
3026
+ "token",
3027
+ "secret",
3028
+ "key",
3029
+ "authorization",
3030
+ "cookie",
3031
+ "session",
3032
+ "credential",
3033
+ "api_key",
3034
+ "apikey",
3035
+ "api-key",
3036
+ "access_token",
3037
+ "refresh_token",
3038
+ "private_key",
3039
+ "client_secret"
3040
+ ]);
3041
+ function isSensitiveKey(key) {
3042
+ const lower = key.toLowerCase();
3043
+ return SENSITIVE_KEYS.has(lower) || Array.from(SENSITIVE_KEYS).some((s) => lower.includes(s));
3044
+ }
3045
+ function sanitizeForLog(data) {
3046
+ if (data === null || data === void 0) {
3047
+ return data;
3048
+ }
3049
+ if (typeof data !== "object") {
3050
+ return data;
3051
+ }
3052
+ if (Array.isArray(data)) {
3053
+ return data.map(sanitizeForLog);
3054
+ }
3055
+ const sanitized = {};
3056
+ for (const [key, value] of Object.entries(data)) {
3057
+ if (isSensitiveKey(key)) {
3058
+ sanitized[key] = "[REDACTED]";
3059
+ } else if (typeof value === "object" && value !== null) {
3060
+ sanitized[key] = sanitizeForLog(value);
3061
+ } else {
3062
+ sanitized[key] = value;
3063
+ }
3064
+ }
3065
+ return sanitized;
3066
+ }
3067
+ function createSafeLogger(prefix) {
3068
+ return {
3069
+ log: (message, data) => {
3070
+ console.log(`${prefix} ${message}`, data ? sanitizeForLog(data) : "");
3071
+ },
3072
+ info: (message, data) => {
3073
+ console.info(`${prefix} ${message}`, data ? sanitizeForLog(data) : "");
3074
+ },
3075
+ warn: (message, data) => {
3076
+ console.warn(`${prefix} ${message}`, data ? sanitizeForLog(data) : "");
3077
+ },
3078
+ error: (message, data) => {
3079
+ console.error(`${prefix} ${message}`, data ? sanitizeForLog(data) : "");
3080
+ }
3081
+ };
3082
+ }
3083
+
3084
+ export { ScaleMuleClient, ScaleMuleProvider, composePhone, createClient, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAuth, useBilling, useContent, useRealtime, useScaleMule, useScaleMuleClient, useUser, validateForm, validators };