@scalemule/nextjs 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,8 @@ This creates all auth endpoints automatically:
34
34
 
35
35
  API keys never reach the browser. Session tokens are HTTP-only cookies.
36
36
 
37
+ Need the cookie/header lifecycle or the proxy-vs-direct decision tree? See [`docs/AUTH_PROXY_PATTERN.md`](./docs/AUTH_PROXY_PATTERN.md).
38
+
37
39
  ## Environment Variables
38
40
 
39
41
  ```bash
@@ -295,7 +297,7 @@ export const { GET, POST, DELETE, PATCH } = createAuthRoutes({
295
297
 
296
298
  ```ts
297
299
  // Read CSRF token from cookie and include in requests
298
- const csrfToken = document.cookie.match(/csrf_token=([^;]+)/)?.[1]
300
+ const csrfToken = document.cookie.match(/sm_csrf=([^;]+)/)?.[1]
299
301
 
300
302
  fetch('/api/auth/login', {
301
303
  method: 'POST',
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a8 as StorageAdapter } from './index-DTWyUcyd.mjs';
1
+ import { a8 as StorageAdapter, A as ApiError } from './index-DTWyUcyd.mjs';
2
2
 
3
3
  /**
4
4
  * ScaleMule API Client
@@ -38,6 +38,12 @@ interface ClientConfig {
38
38
  enableAccountSwitcher?: boolean;
39
39
  /** Privacy level for account switcher */
40
40
  accountSwitcherPrivacy?: 'full' | 'masked' | 'minimal';
41
+ /** Optional: Callback when a 401 auto-refresh starts. */
42
+ onRefreshStart?: () => void;
43
+ /** Optional: Callback when a 401 auto-refresh finishes. */
44
+ onRefreshEnd?: () => void;
45
+ /** Optional: Callback when an automatic 401 refresh fails. */
46
+ onAutoRefreshFailed?: (error: ApiError) => void;
41
47
  }
42
48
  interface RequestOptions extends RequestInit {
43
49
  /** Skip adding auth headers (for public endpoints) */
@@ -48,6 +54,8 @@ interface RequestOptions extends RequestInit {
48
54
  retries?: number;
49
55
  /** Skip retries */
50
56
  skipRetry?: boolean;
57
+ /** Internal flag to prevent infinite refresh loops */
58
+ isAutoRefresh?: boolean;
51
59
  }
52
60
  /**
53
61
  * ScaleMule API Client
@@ -69,6 +77,10 @@ declare class ScaleMuleClient {
69
77
  private sessionGate;
70
78
  private resolveSessionGate;
71
79
  private workspaceId;
80
+ private refreshPromise;
81
+ private onRefreshStart?;
82
+ private onRefreshEnd?;
83
+ private onAutoRefreshFailed?;
72
84
  constructor(config: ClientConfig);
73
85
  /**
74
86
  * Sync offline queue when coming back online
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a8 as StorageAdapter } from './index-DTWyUcyd.js';
1
+ import { a8 as StorageAdapter, A as ApiError } from './index-DTWyUcyd.js';
2
2
 
3
3
  /**
4
4
  * ScaleMule API Client
@@ -38,6 +38,12 @@ interface ClientConfig {
38
38
  enableAccountSwitcher?: boolean;
39
39
  /** Privacy level for account switcher */
40
40
  accountSwitcherPrivacy?: 'full' | 'masked' | 'minimal';
41
+ /** Optional: Callback when a 401 auto-refresh starts. */
42
+ onRefreshStart?: () => void;
43
+ /** Optional: Callback when a 401 auto-refresh finishes. */
44
+ onRefreshEnd?: () => void;
45
+ /** Optional: Callback when an automatic 401 refresh fails. */
46
+ onAutoRefreshFailed?: (error: ApiError) => void;
41
47
  }
42
48
  interface RequestOptions extends RequestInit {
43
49
  /** Skip adding auth headers (for public endpoints) */
@@ -48,6 +54,8 @@ interface RequestOptions extends RequestInit {
48
54
  retries?: number;
49
55
  /** Skip retries */
50
56
  skipRetry?: boolean;
57
+ /** Internal flag to prevent infinite refresh loops */
58
+ isAutoRefresh?: boolean;
51
59
  }
52
60
  /**
53
61
  * ScaleMule API Client
@@ -69,6 +77,10 @@ declare class ScaleMuleClient {
69
77
  private sessionGate;
70
78
  private resolveSessionGate;
71
79
  private workspaceId;
80
+ private refreshPromise;
81
+ private onRefreshStart?;
82
+ private onRefreshEnd?;
83
+ private onAutoRefreshFailed?;
72
84
  constructor(config: ClientConfig);
73
85
  /**
74
86
  * Sync offline queue when coming back online
package/dist/client.js CHANGED
@@ -278,6 +278,7 @@ var ScaleMuleClient = class {
278
278
  this.sessionGate = null;
279
279
  this.resolveSessionGate = null;
280
280
  this.workspaceId = null;
281
+ this.refreshPromise = null;
281
282
  this.apiKey = config.apiKey;
282
283
  this.applicationId = config.applicationId || null;
283
284
  this.gatewayUrl = resolveGatewayUrl(config);
@@ -285,6 +286,9 @@ var ScaleMuleClient = class {
285
286
  this.storage = config.storage || createDefaultStorage();
286
287
  this.enableRateLimitQueue = config.enableRateLimitQueue || false;
287
288
  this.enableOfflineQueue = config.enableOfflineQueue || false;
289
+ this.onRefreshStart = config.onRefreshStart;
290
+ this.onRefreshEnd = config.onRefreshEnd;
291
+ this.onAutoRefreshFailed = config.onAutoRefreshFailed;
288
292
  if (this.enableRateLimitQueue) {
289
293
  this.rateLimitQueue = new RateLimitQueue();
290
294
  }
@@ -503,6 +507,10 @@ var ScaleMuleClient = class {
503
507
  signal: controller.signal
504
508
  });
505
509
  clearTimeout(timeoutId);
510
+ const rotatedToken = response.headers.get("x-rotated-session-token");
511
+ if (rotatedToken && this.sessionToken && this.userId) {
512
+ this.setSession(rotatedToken, this.userId);
513
+ }
506
514
  const text = await response.text();
507
515
  let responseData = null;
508
516
  try {
@@ -512,6 +520,30 @@ var ScaleMuleClient = class {
512
520
  if (!response.ok) {
513
521
  const rawError = responseData?.error;
514
522
  const error = rawError && typeof rawError === "object" ? rawError : { code: `HTTP_${response.status}`, message: typeof rawError === "string" ? rawError : responseData?.message || text || response.statusText };
523
+ if (response.status === 401 && this.sessionToken && !options.isAutoRefresh) {
524
+ if (this.debug) console.log("[ScaleMule] 401 received, attempting auto-refresh...");
525
+ if (!this.refreshPromise) {
526
+ this.onRefreshStart?.();
527
+ this.refreshPromise = this.post("/api/auth/refresh", {}, { isAutoRefresh: true });
528
+ }
529
+ try {
530
+ await this.refreshPromise;
531
+ if (this.debug) console.log("[ScaleMule] Auto-refresh succeeded, retrying original request...");
532
+ attempt--;
533
+ if (this.sessionToken) {
534
+ headers.set("Authorization", `Bearer ${this.sessionToken}`);
535
+ }
536
+ continue;
537
+ } catch (refreshErr) {
538
+ if (this.debug) console.error("[ScaleMule] Auto-refresh failed:", refreshErr);
539
+ const refreshApiError = refreshErr instanceof ScaleMuleApiError ? { code: refreshErr.code, message: refreshErr.message, field: refreshErr.field } : { code: "REFRESH_FAILED", message: "Auto-refresh failed" };
540
+ this.onAutoRefreshFailed?.(refreshApiError);
541
+ throw new ScaleMuleApiError(error);
542
+ } finally {
543
+ this.refreshPromise = null;
544
+ this.onRefreshEnd?.();
545
+ }
546
+ }
515
547
  if (attempt < maxRetries && RETRYABLE_STATUS_CODES.has(response.status)) {
516
548
  lastError = error;
517
549
  const delay = getBackoffDelay(attempt);
package/dist/client.mjs CHANGED
@@ -276,6 +276,7 @@ var ScaleMuleClient = class {
276
276
  this.sessionGate = null;
277
277
  this.resolveSessionGate = null;
278
278
  this.workspaceId = null;
279
+ this.refreshPromise = null;
279
280
  this.apiKey = config.apiKey;
280
281
  this.applicationId = config.applicationId || null;
281
282
  this.gatewayUrl = resolveGatewayUrl(config);
@@ -283,6 +284,9 @@ var ScaleMuleClient = class {
283
284
  this.storage = config.storage || createDefaultStorage();
284
285
  this.enableRateLimitQueue = config.enableRateLimitQueue || false;
285
286
  this.enableOfflineQueue = config.enableOfflineQueue || false;
287
+ this.onRefreshStart = config.onRefreshStart;
288
+ this.onRefreshEnd = config.onRefreshEnd;
289
+ this.onAutoRefreshFailed = config.onAutoRefreshFailed;
286
290
  if (this.enableRateLimitQueue) {
287
291
  this.rateLimitQueue = new RateLimitQueue();
288
292
  }
@@ -501,6 +505,10 @@ var ScaleMuleClient = class {
501
505
  signal: controller.signal
502
506
  });
503
507
  clearTimeout(timeoutId);
508
+ const rotatedToken = response.headers.get("x-rotated-session-token");
509
+ if (rotatedToken && this.sessionToken && this.userId) {
510
+ this.setSession(rotatedToken, this.userId);
511
+ }
504
512
  const text = await response.text();
505
513
  let responseData = null;
506
514
  try {
@@ -510,6 +518,30 @@ var ScaleMuleClient = class {
510
518
  if (!response.ok) {
511
519
  const rawError = responseData?.error;
512
520
  const error = rawError && typeof rawError === "object" ? rawError : { code: `HTTP_${response.status}`, message: typeof rawError === "string" ? rawError : responseData?.message || text || response.statusText };
521
+ if (response.status === 401 && this.sessionToken && !options.isAutoRefresh) {
522
+ if (this.debug) console.log("[ScaleMule] 401 received, attempting auto-refresh...");
523
+ if (!this.refreshPromise) {
524
+ this.onRefreshStart?.();
525
+ this.refreshPromise = this.post("/api/auth/refresh", {}, { isAutoRefresh: true });
526
+ }
527
+ try {
528
+ await this.refreshPromise;
529
+ if (this.debug) console.log("[ScaleMule] Auto-refresh succeeded, retrying original request...");
530
+ attempt--;
531
+ if (this.sessionToken) {
532
+ headers.set("Authorization", `Bearer ${this.sessionToken}`);
533
+ }
534
+ continue;
535
+ } catch (refreshErr) {
536
+ if (this.debug) console.error("[ScaleMule] Auto-refresh failed:", refreshErr);
537
+ const refreshApiError = refreshErr instanceof ScaleMuleApiError ? { code: refreshErr.code, message: refreshErr.message, field: refreshErr.field } : { code: "REFRESH_FAILED", message: "Auto-refresh failed" };
538
+ this.onAutoRefreshFailed?.(refreshApiError);
539
+ throw new ScaleMuleApiError(error);
540
+ } finally {
541
+ this.refreshPromise = null;
542
+ this.onRefreshEnd?.();
543
+ }
544
+ }
513
545
  if (attempt < maxRetries && RETRYABLE_STATUS_CODES.has(response.status)) {
514
546
  lastError = error;
515
547
  const delay = getBackoffDelay(attempt);
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, FileStatus } from '@scalemule/sdk';
2
+ import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, TtsService, TtsJobStatus, FileStatus } from '@scalemule/sdk';
3
3
  import * as React from 'react';
4
4
  import { ReactNode, ReactElement } from 'react';
5
5
  import { MoneyClient } from '@scalemule/money';
@@ -166,6 +166,8 @@ interface ScaleMuleContextValue {
166
166
  video: VideoService;
167
167
  /** Base SDK audio service — exposed for `useMedia()` and `audio.uploadViaStorage()` */
168
168
  audio: AudioService;
169
+ /** Base SDK TTS service — exposed for `useTtsJob()` and direct narration requests */
170
+ tts: TtsService;
169
171
  /**
170
172
  * Default media policy for `useMedia()` calls. Set via
171
173
  * `<ScaleMuleProvider mediaPolicy="…">`; per-call overrides win.
@@ -360,6 +362,18 @@ interface UseAudioReturn {
360
362
  }
361
363
  declare function useAudio(): UseAudioReturn;
362
364
 
365
+ interface UseTtsJobOptions {
366
+ enabled?: boolean;
367
+ pollIntervalMs?: number;
368
+ }
369
+ interface UseTtsJobReturn {
370
+ job: TtsJobStatus | null;
371
+ loading: boolean;
372
+ error: ApiError | null;
373
+ refresh: () => Promise<TtsJobStatus | null>;
374
+ }
375
+ declare function useTtsJob(jobId: string | null | undefined, options?: UseTtsJobOptions): UseTtsJobReturn;
376
+
363
377
  /**
364
378
  * Conversation kinds recognized by the chat realtime channel naming scheme.
365
379
  * Matches `broadcast_to_conversation` in `ms/scalemule-chat/src/realtime.rs`.
@@ -1032,4 +1046,4 @@ declare function createSafeLogger(prefix: string): {
1032
1046
  error: (message: string, data?: unknown) => void;
1033
1047
  };
1034
1048
 
1035
- export { ApiError$1 as ApiError, type AudioFile, type AudioUploadResult, type ConversationKind, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaPolicy, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleMedia, type ScaleMuleMediaProps, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, type UseAudioReturn, type UseAudioUploadOptions, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
1049
+ export { ApiError$1 as ApiError, type AudioFile, type AudioUploadResult, type ConversationKind, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaPolicy, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleMedia, type ScaleMuleMediaProps, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, type UseAudioReturn, type UseAudioUploadOptions, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, type UseTtsJobOptions, type UseTtsJobReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useTtsJob, useUser, validateForm, validators };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, FileStatus } from '@scalemule/sdk';
2
+ import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, TtsService, TtsJobStatus, FileStatus } from '@scalemule/sdk';
3
3
  import * as React from 'react';
4
4
  import { ReactNode, ReactElement } from 'react';
5
5
  import { MoneyClient } from '@scalemule/money';
@@ -166,6 +166,8 @@ interface ScaleMuleContextValue {
166
166
  video: VideoService;
167
167
  /** Base SDK audio service — exposed for `useMedia()` and `audio.uploadViaStorage()` */
168
168
  audio: AudioService;
169
+ /** Base SDK TTS service — exposed for `useTtsJob()` and direct narration requests */
170
+ tts: TtsService;
169
171
  /**
170
172
  * Default media policy for `useMedia()` calls. Set via
171
173
  * `<ScaleMuleProvider mediaPolicy="…">`; per-call overrides win.
@@ -360,6 +362,18 @@ interface UseAudioReturn {
360
362
  }
361
363
  declare function useAudio(): UseAudioReturn;
362
364
 
365
+ interface UseTtsJobOptions {
366
+ enabled?: boolean;
367
+ pollIntervalMs?: number;
368
+ }
369
+ interface UseTtsJobReturn {
370
+ job: TtsJobStatus | null;
371
+ loading: boolean;
372
+ error: ApiError | null;
373
+ refresh: () => Promise<TtsJobStatus | null>;
374
+ }
375
+ declare function useTtsJob(jobId: string | null | undefined, options?: UseTtsJobOptions): UseTtsJobReturn;
376
+
363
377
  /**
364
378
  * Conversation kinds recognized by the chat realtime channel naming scheme.
365
379
  * Matches `broadcast_to_conversation` in `ms/scalemule-chat/src/realtime.rs`.
@@ -1032,4 +1046,4 @@ declare function createSafeLogger(prefix: string): {
1032
1046
  error: (message: string, data?: unknown) => void;
1033
1047
  };
1034
1048
 
1035
- export { ApiError$1 as ApiError, type AudioFile, type AudioUploadResult, type ConversationKind, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaPolicy, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleMedia, type ScaleMuleMediaProps, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, type UseAudioReturn, type UseAudioUploadOptions, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
1049
+ export { ApiError$1 as ApiError, type AudioFile, type AudioUploadResult, type ConversationKind, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaPolicy, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleMedia, type ScaleMuleMediaProps, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, type UseAudioReturn, type UseAudioUploadOptions, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, type UseTtsJobOptions, type UseTtsJobReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useTtsJob, useUser, validateForm, validators };
package/dist/index.js CHANGED
@@ -7023,6 +7023,7 @@ var ScaleMuleClient2 = class {
7023
7023
  this.sessionGate = null;
7024
7024
  this.resolveSessionGate = null;
7025
7025
  this.workspaceId = null;
7026
+ this.refreshPromise = null;
7026
7027
  this.apiKey = config.apiKey;
7027
7028
  this.applicationId = config.applicationId || null;
7028
7029
  this.gatewayUrl = resolveGatewayUrl(config);
@@ -7030,6 +7031,9 @@ var ScaleMuleClient2 = class {
7030
7031
  this.storage = config.storage || createDefaultStorage2();
7031
7032
  this.enableRateLimitQueue = config.enableRateLimitQueue || false;
7032
7033
  this.enableOfflineQueue = config.enableOfflineQueue || false;
7034
+ this.onRefreshStart = config.onRefreshStart;
7035
+ this.onRefreshEnd = config.onRefreshEnd;
7036
+ this.onAutoRefreshFailed = config.onAutoRefreshFailed;
7033
7037
  if (this.enableRateLimitQueue) {
7034
7038
  this.rateLimitQueue = new RateLimitQueue2();
7035
7039
  }
@@ -7248,6 +7252,10 @@ var ScaleMuleClient2 = class {
7248
7252
  signal: controller.signal
7249
7253
  });
7250
7254
  clearTimeout(timeoutId);
7255
+ const rotatedToken = response.headers.get("x-rotated-session-token");
7256
+ if (rotatedToken && this.sessionToken && this.userId) {
7257
+ this.setSession(rotatedToken, this.userId);
7258
+ }
7251
7259
  const text = await response.text();
7252
7260
  let responseData = null;
7253
7261
  try {
@@ -7257,6 +7265,30 @@ var ScaleMuleClient2 = class {
7257
7265
  if (!response.ok) {
7258
7266
  const rawError = responseData?.error;
7259
7267
  const error = rawError && typeof rawError === "object" ? rawError : { code: `HTTP_${response.status}`, message: typeof rawError === "string" ? rawError : responseData?.message || text || response.statusText };
7268
+ if (response.status === 401 && this.sessionToken && !options.isAutoRefresh) {
7269
+ if (this.debug) console.log("[ScaleMule] 401 received, attempting auto-refresh...");
7270
+ if (!this.refreshPromise) {
7271
+ this.onRefreshStart?.();
7272
+ this.refreshPromise = this.post("/api/auth/refresh", {}, { isAutoRefresh: true });
7273
+ }
7274
+ try {
7275
+ await this.refreshPromise;
7276
+ if (this.debug) console.log("[ScaleMule] Auto-refresh succeeded, retrying original request...");
7277
+ attempt--;
7278
+ if (this.sessionToken) {
7279
+ headers.set("Authorization", `Bearer ${this.sessionToken}`);
7280
+ }
7281
+ continue;
7282
+ } catch (refreshErr) {
7283
+ if (this.debug) console.error("[ScaleMule] Auto-refresh failed:", refreshErr);
7284
+ const refreshApiError = refreshErr instanceof ScaleMuleApiError ? { code: refreshErr.code, message: refreshErr.message, field: refreshErr.field } : { code: "REFRESH_FAILED", message: "Auto-refresh failed" };
7285
+ this.onAutoRefreshFailed?.(refreshApiError);
7286
+ throw new ScaleMuleApiError(error);
7287
+ } finally {
7288
+ this.refreshPromise = null;
7289
+ this.onRefreshEnd?.();
7290
+ }
7291
+ }
7260
7292
  if (attempt < maxRetries && RETRYABLE_STATUS_CODES3.has(response.status)) {
7261
7293
  lastError = error;
7262
7294
  const delay = getBackoffDelay2(attempt);
@@ -7772,6 +7804,7 @@ function ScaleMuleProvider({
7772
7804
  photo: baseClient.photo,
7773
7805
  video: baseClient.video,
7774
7806
  audio: baseClient.audio,
7807
+ tts: baseClient.tts,
7775
7808
  mediaPolicy: effectiveMediaPolicy,
7776
7809
  user,
7777
7810
  setUser: handleSetUser,
@@ -9160,7 +9193,7 @@ function useAudio() {
9160
9193
  return null;
9161
9194
  }
9162
9195
  try {
9163
- const details = await client.get(`/v1/audios/${audioId}`);
9196
+ const details = await client.get(`/v1/audio/${audioId}`);
9164
9197
  if (details.status === "ready" && details.url) {
9165
9198
  return details.url;
9166
9199
  }
@@ -9222,7 +9255,7 @@ function useAudio() {
9222
9255
  const enriched = await Promise.all(
9223
9256
  audioFiles.map(async (file) => {
9224
9257
  try {
9225
- const details = await client.get(`/v1/audios/${file.id}`);
9258
+ const details = await client.get(`/v1/audio/${file.id}`);
9226
9259
  return {
9227
9260
  audio_id: details.id,
9228
9261
  file_id: file.id,
@@ -9258,7 +9291,7 @@ function useAudio() {
9258
9291
  setError(null);
9259
9292
  try {
9260
9293
  const [audioDeleteResult, storageDeleteResult] = await Promise.allSettled([
9261
- client.delete(`/v1/audios/${fileId}`),
9294
+ client.delete(`/v1/audio/${fileId}`),
9262
9295
  client.delete(`/v1/storage/files/${fileId}`)
9263
9296
  ]);
9264
9297
  if (storageDeleteResult.status === "rejected" && !isNotFoundError(storageDeleteResult.reason)) {
@@ -9285,6 +9318,63 @@ function useAudio() {
9285
9318
  loading
9286
9319
  };
9287
9320
  }
9321
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
9322
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["ready", "failed"]);
9323
+ function useTtsJob(jobId, options) {
9324
+ const { tts } = useScaleMule();
9325
+ const [job, setJob] = React.useState(null);
9326
+ const [loading, setLoading] = React.useState(false);
9327
+ const [error, setError] = React.useState(null);
9328
+ const refresh = React.useCallback(async () => {
9329
+ if (!jobId) {
9330
+ setJob(null);
9331
+ return null;
9332
+ }
9333
+ setLoading(true);
9334
+ setError(null);
9335
+ try {
9336
+ const result = await tts.getJob(jobId);
9337
+ if (!result) {
9338
+ const apiError = { code: "tts_job_error", message: "Failed to load TTS job", status: 500 };
9339
+ setError(apiError);
9340
+ return null;
9341
+ }
9342
+ if (result.error || !result.data) {
9343
+ const apiError = result.error ?? { code: "tts_job_error", message: "Failed to load TTS job", status: 500 };
9344
+ setError(apiError);
9345
+ return null;
9346
+ }
9347
+ setJob(result.data);
9348
+ return result.data;
9349
+ } finally {
9350
+ setLoading(false);
9351
+ }
9352
+ }, [jobId, tts]);
9353
+ React.useEffect(() => {
9354
+ if (!jobId || options?.enabled === false) {
9355
+ setJob(null);
9356
+ setLoading(false);
9357
+ return;
9358
+ }
9359
+ let cancelled = false;
9360
+ let timeoutId = null;
9361
+ const poll = async () => {
9362
+ const next = await refresh();
9363
+ if (cancelled || !next || TERMINAL_STATUSES.has(next.status)) {
9364
+ return;
9365
+ }
9366
+ timeoutId = window.setTimeout(poll, options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
9367
+ };
9368
+ poll();
9369
+ return () => {
9370
+ cancelled = true;
9371
+ if (timeoutId != null) {
9372
+ window.clearTimeout(timeoutId);
9373
+ }
9374
+ };
9375
+ }, [jobId, options?.enabled, options?.pollIntervalMs, refresh]);
9376
+ return { job, loading, error, refresh };
9377
+ }
9288
9378
  function useMedia() {
9289
9379
  const { storage, photo, video, audio, mediaPolicy: providerDefaultPolicy } = useScaleMule();
9290
9380
  const [uploading, setUploading] = React.useState(false);
@@ -11525,6 +11615,7 @@ exports.useRealtime = useRealtime;
11525
11615
  exports.useScaleMule = useScaleMule;
11526
11616
  exports.useScaleMuleClient = useScaleMuleClient;
11527
11617
  exports.useShare = useShare;
11618
+ exports.useTtsJob = useTtsJob;
11528
11619
  exports.useUser = useUser;
11529
11620
  exports.validateForm = validateForm;
11530
11621
  exports.validators = validators;
package/dist/index.mjs CHANGED
@@ -7003,6 +7003,7 @@ var ScaleMuleClient2 = class {
7003
7003
  this.sessionGate = null;
7004
7004
  this.resolveSessionGate = null;
7005
7005
  this.workspaceId = null;
7006
+ this.refreshPromise = null;
7006
7007
  this.apiKey = config.apiKey;
7007
7008
  this.applicationId = config.applicationId || null;
7008
7009
  this.gatewayUrl = resolveGatewayUrl(config);
@@ -7010,6 +7011,9 @@ var ScaleMuleClient2 = class {
7010
7011
  this.storage = config.storage || createDefaultStorage2();
7011
7012
  this.enableRateLimitQueue = config.enableRateLimitQueue || false;
7012
7013
  this.enableOfflineQueue = config.enableOfflineQueue || false;
7014
+ this.onRefreshStart = config.onRefreshStart;
7015
+ this.onRefreshEnd = config.onRefreshEnd;
7016
+ this.onAutoRefreshFailed = config.onAutoRefreshFailed;
7013
7017
  if (this.enableRateLimitQueue) {
7014
7018
  this.rateLimitQueue = new RateLimitQueue2();
7015
7019
  }
@@ -7228,6 +7232,10 @@ var ScaleMuleClient2 = class {
7228
7232
  signal: controller.signal
7229
7233
  });
7230
7234
  clearTimeout(timeoutId);
7235
+ const rotatedToken = response.headers.get("x-rotated-session-token");
7236
+ if (rotatedToken && this.sessionToken && this.userId) {
7237
+ this.setSession(rotatedToken, this.userId);
7238
+ }
7231
7239
  const text = await response.text();
7232
7240
  let responseData = null;
7233
7241
  try {
@@ -7237,6 +7245,30 @@ var ScaleMuleClient2 = class {
7237
7245
  if (!response.ok) {
7238
7246
  const rawError = responseData?.error;
7239
7247
  const error = rawError && typeof rawError === "object" ? rawError : { code: `HTTP_${response.status}`, message: typeof rawError === "string" ? rawError : responseData?.message || text || response.statusText };
7248
+ if (response.status === 401 && this.sessionToken && !options.isAutoRefresh) {
7249
+ if (this.debug) console.log("[ScaleMule] 401 received, attempting auto-refresh...");
7250
+ if (!this.refreshPromise) {
7251
+ this.onRefreshStart?.();
7252
+ this.refreshPromise = this.post("/api/auth/refresh", {}, { isAutoRefresh: true });
7253
+ }
7254
+ try {
7255
+ await this.refreshPromise;
7256
+ if (this.debug) console.log("[ScaleMule] Auto-refresh succeeded, retrying original request...");
7257
+ attempt--;
7258
+ if (this.sessionToken) {
7259
+ headers.set("Authorization", `Bearer ${this.sessionToken}`);
7260
+ }
7261
+ continue;
7262
+ } catch (refreshErr) {
7263
+ if (this.debug) console.error("[ScaleMule] Auto-refresh failed:", refreshErr);
7264
+ const refreshApiError = refreshErr instanceof ScaleMuleApiError ? { code: refreshErr.code, message: refreshErr.message, field: refreshErr.field } : { code: "REFRESH_FAILED", message: "Auto-refresh failed" };
7265
+ this.onAutoRefreshFailed?.(refreshApiError);
7266
+ throw new ScaleMuleApiError(error);
7267
+ } finally {
7268
+ this.refreshPromise = null;
7269
+ this.onRefreshEnd?.();
7270
+ }
7271
+ }
7240
7272
  if (attempt < maxRetries && RETRYABLE_STATUS_CODES3.has(response.status)) {
7241
7273
  lastError = error;
7242
7274
  const delay = getBackoffDelay2(attempt);
@@ -7752,6 +7784,7 @@ function ScaleMuleProvider({
7752
7784
  photo: baseClient.photo,
7753
7785
  video: baseClient.video,
7754
7786
  audio: baseClient.audio,
7787
+ tts: baseClient.tts,
7755
7788
  mediaPolicy: effectiveMediaPolicy,
7756
7789
  user,
7757
7790
  setUser: handleSetUser,
@@ -9140,7 +9173,7 @@ function useAudio() {
9140
9173
  return null;
9141
9174
  }
9142
9175
  try {
9143
- const details = await client.get(`/v1/audios/${audioId}`);
9176
+ const details = await client.get(`/v1/audio/${audioId}`);
9144
9177
  if (details.status === "ready" && details.url) {
9145
9178
  return details.url;
9146
9179
  }
@@ -9202,7 +9235,7 @@ function useAudio() {
9202
9235
  const enriched = await Promise.all(
9203
9236
  audioFiles.map(async (file) => {
9204
9237
  try {
9205
- const details = await client.get(`/v1/audios/${file.id}`);
9238
+ const details = await client.get(`/v1/audio/${file.id}`);
9206
9239
  return {
9207
9240
  audio_id: details.id,
9208
9241
  file_id: file.id,
@@ -9238,7 +9271,7 @@ function useAudio() {
9238
9271
  setError(null);
9239
9272
  try {
9240
9273
  const [audioDeleteResult, storageDeleteResult] = await Promise.allSettled([
9241
- client.delete(`/v1/audios/${fileId}`),
9274
+ client.delete(`/v1/audio/${fileId}`),
9242
9275
  client.delete(`/v1/storage/files/${fileId}`)
9243
9276
  ]);
9244
9277
  if (storageDeleteResult.status === "rejected" && !isNotFoundError(storageDeleteResult.reason)) {
@@ -9265,6 +9298,63 @@ function useAudio() {
9265
9298
  loading
9266
9299
  };
9267
9300
  }
9301
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
9302
+ var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["ready", "failed"]);
9303
+ function useTtsJob(jobId, options) {
9304
+ const { tts } = useScaleMule();
9305
+ const [job, setJob] = useState(null);
9306
+ const [loading, setLoading] = useState(false);
9307
+ const [error, setError] = useState(null);
9308
+ const refresh = useCallback(async () => {
9309
+ if (!jobId) {
9310
+ setJob(null);
9311
+ return null;
9312
+ }
9313
+ setLoading(true);
9314
+ setError(null);
9315
+ try {
9316
+ const result = await tts.getJob(jobId);
9317
+ if (!result) {
9318
+ const apiError = { code: "tts_job_error", message: "Failed to load TTS job", status: 500 };
9319
+ setError(apiError);
9320
+ return null;
9321
+ }
9322
+ if (result.error || !result.data) {
9323
+ const apiError = result.error ?? { code: "tts_job_error", message: "Failed to load TTS job", status: 500 };
9324
+ setError(apiError);
9325
+ return null;
9326
+ }
9327
+ setJob(result.data);
9328
+ return result.data;
9329
+ } finally {
9330
+ setLoading(false);
9331
+ }
9332
+ }, [jobId, tts]);
9333
+ useEffect(() => {
9334
+ if (!jobId || options?.enabled === false) {
9335
+ setJob(null);
9336
+ setLoading(false);
9337
+ return;
9338
+ }
9339
+ let cancelled = false;
9340
+ let timeoutId = null;
9341
+ const poll = async () => {
9342
+ const next = await refresh();
9343
+ if (cancelled || !next || TERMINAL_STATUSES.has(next.status)) {
9344
+ return;
9345
+ }
9346
+ timeoutId = window.setTimeout(poll, options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
9347
+ };
9348
+ poll();
9349
+ return () => {
9350
+ cancelled = true;
9351
+ if (timeoutId != null) {
9352
+ window.clearTimeout(timeoutId);
9353
+ }
9354
+ };
9355
+ }, [jobId, options?.enabled, options?.pollIntervalMs, refresh]);
9356
+ return { job, loading, error, refresh };
9357
+ }
9268
9358
  function useMedia() {
9269
9359
  const { storage, photo, video, audio, mediaPolicy: providerDefaultPolicy } = useScaleMule();
9270
9360
  const [uploading, setUploading] = useState(false);
@@ -11470,4 +11560,4 @@ function createSafeLogger(prefix) {
11470
11560
  };
11471
11561
  }
11472
11562
 
11473
- export { FeedbackWidget, ScaleMuleApiError, ScaleMuleClient2 as ScaleMuleClient, ScaleMuleMedia, ScaleMuleProvider, composePhone, createClient, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
11563
+ export { FeedbackWidget, ScaleMuleApiError, ScaleMuleClient2 as ScaleMuleClient, ScaleMuleMedia, ScaleMuleProvider, composePhone, createClient, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useTtsJob, useUser, validateForm, validators };