@scalemule/nextjs 0.1.7 → 0.1.9

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
@@ -132,7 +132,67 @@ function LoginPage() {
132
132
  }
133
133
  ```
134
134
 
135
- ### `useContent()`
135
+ ### `useMedia()` — recommended for image / video / audio uploads
136
+
137
+ One hook covers every MIME type, defaults to private, and auto-routes images
138
+ through the photo pipeline (optimization, transform breakpoints) and videos
139
+ through the video pipeline (HLS transcoding). Drop the result `file_id` into
140
+ a chat message or render it with `<ScaleMuleMedia />` and progressive
141
+ enhancement just works.
142
+
143
+ ```tsx
144
+ import { useMedia, ScaleMuleMedia } from '@scalemule/nextjs'
145
+
146
+ function Composer({ conversationId }: { conversationId: string }) {
147
+ const { upload, uploading } = useMedia()
148
+
149
+ async function handleFile(file: File) {
150
+ const result = await upload(file)
151
+ // result.file_id is private by default; post the message with it.
152
+ await postMessage({ conversationId, attachments: [{ file_id: result.file_id }] })
153
+ }
154
+
155
+ return <input type="file" disabled={uploading} onChange={(e) => handleFile(e.target.files![0])} />
156
+ }
157
+
158
+ function Attachment({ fileId, conversationId, mimeType }: {
159
+ fileId: string; conversationId: string; mimeType: string
160
+ }) {
161
+ // <ScaleMuleMedia /> uses useFileStatus internally; with conversationId
162
+ // it subscribes to the conversation channel for live optimize/transcode
163
+ // updates instead of polling.
164
+ return (
165
+ <ScaleMuleMedia
166
+ fileId={fileId}
167
+ mimeType={mimeType}
168
+ conversationId={conversationId}
169
+ />
170
+ )
171
+ }
172
+ ```
173
+
174
+ See [`docs/MEDIA-UPLOADS.md`](https://github.com/scalemule/scalemule-repos/blob/main/docs/MEDIA-UPLOADS.md)
175
+ for the full decision tree (which hook for which app, fast/safe policy modes,
176
+ private vs public visibility, anti-patterns).
177
+
178
+ ### `useFileStatus()` — read-side progressive enhancement
179
+
180
+ ```tsx
181
+ import { useFileStatus } from '@scalemule/nextjs'
182
+
183
+ // Chat surface — subscribe to the conversation channel for push updates.
184
+ const { status, isReady } = useFileStatus({ fileId, conversationId })
185
+
186
+ // Non-chat — pull-only with optional polling.
187
+ const { status, isReady } = useFileStatus({ fileId, pollIntervalMs: 2000 })
188
+ ```
189
+
190
+ ### `useContent()` — generic file uploads (legacy)
191
+
192
+ > **Deprecated for chat / progressive media.** Use `useMedia()` instead.
193
+ > `useContent()` remains available for plain non-image/video file lists
194
+ > (documents, downloads). It does not auto-route through the photo/video
195
+ > pipelines and does not enforce private-by-default for chat surfaces.
136
196
 
137
197
  ```tsx
138
198
  import { useContent } from '@scalemule/nextjs'
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, FileStatus } from '@scalemule/sdk';
2
+ import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, 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';
@@ -164,6 +164,8 @@ interface ScaleMuleContextValue {
164
164
  photo: PhotoService;
165
165
  /** Base SDK video service — exposed for `useMedia()` and `video.uploadViaStorage()` */
166
166
  video: VideoService;
167
+ /** Base SDK audio service — exposed for `useMedia()` and `audio.uploadViaStorage()` */
168
+ audio: AudioService;
167
169
  /**
168
170
  * Default media policy for `useMedia()` calls. Set via
169
171
  * `<ScaleMuleProvider mediaPolicy="…">`; per-call overrides win.
@@ -322,25 +324,39 @@ interface UseContentOptions {
322
324
  */
323
325
  declare function useContent(options?: UseContentOptions): UseContentReturn;
324
326
 
327
+ /**
328
+ * Conversation kinds recognized by the chat realtime channel naming scheme.
329
+ * Matches `broadcast_to_conversation` in `ms/scalemule-chat/src/realtime.rs`.
330
+ */
331
+ type ConversationKind = 'standard' | 'large_room' | 'broadcast' | 'support';
325
332
  interface UseFileStatusOptions {
326
333
  /** Storage file_id to read status for. */
327
334
  fileId: string | null | undefined;
328
335
  /**
329
336
  * Optional poll interval in milliseconds. If set, the hook re-fetches
330
- * status every `pollIntervalMs`. Useful while waiting for transcode /
331
- * optimization to complete. Pass `null` (default) for a one-shot read.
332
- *
333
- * Polling stops automatically once `scan.status === 'clean'` AND the
334
- * caller's expected pipeline is done (`urls.optimized` returns 200 for
335
- * images, `urls.hls` returns 200 for videos). Today the hook can only
336
- * detect scan clean — broader pipeline-done detection lands when Phase 3
337
- * enriches the optimize/transcode response fields.
337
+ * status every `pollIntervalMs` until scan goes clean. Useful for
338
+ * non-chat surfaces or as a fallback alongside `conversationId` push.
338
339
  */
339
340
  pollIntervalMs?: number | null;
341
+ /** Disable the hook (don't fetch). Useful when `fileId` is conditional. */
342
+ disabled?: boolean;
340
343
  /**
341
- * Disable the hook (don't fetch). Useful when `fileId` is conditional.
344
+ * Chat-surface push variant: subscribe to the conversation's realtime
345
+ * channel and refresh when a `file_status_changed` event arrives for
346
+ * `fileId`. Auth rides the existing per-conversation channel ACL.
342
347
  */
343
- disabled?: boolean;
348
+ conversationId?: string | null;
349
+ /**
350
+ * Conversation kind, controls the channel name prefix:
351
+ * - `standard` (default) → `conversation:{id}`
352
+ * - `large_room` → `conversation:lr:{id}`
353
+ * - `broadcast` → `conversation:bc:{id}`
354
+ * - `support` → `conversation:support:{id}`
355
+ *
356
+ * If you only have a `messageId`, look up the conversation first via
357
+ * `client.chat.getMessage(messageId)` and pass the result here.
358
+ */
359
+ conversationKind?: ConversationKind;
344
360
  }
345
361
  interface UseFileStatusReturn {
346
362
  /** The latest status response, or null on first render / disabled. */
@@ -360,29 +376,39 @@ interface UseFileStatusReturn {
360
376
  refresh: () => Promise<void>;
361
377
  }
362
378
  /**
363
- * Subscribes to {@link FileStatus} for a single file. Today this is a
364
- * pull-only hook — single fetch by default, optional polling.
379
+ * Subscribes to {@link FileStatus} for a single file.
365
380
  *
366
- * Phase 3 of the realtime-chat media pipeline ADR will add a push variant
367
- * for chat surfaces — `useFileStatus({ messageId })` will subscribe to
368
- * `file.status` events on the existing per-conversation realtime channel
369
- * via the `scalemule-chat` translation bridge (P5'). Until that lands,
370
- * customers using this hook from chat surfaces should pass `pollIntervalMs`
371
- * in the 1–3 second range while a media pipeline is expected to be running,
372
- * then drop the polling once `isReady` is true.
381
+ * Three call shapes:
373
382
  *
374
- * @example
383
+ * 1. **Pull-only** — `useFileStatus({ fileId })` plus optional `pollIntervalMs`.
384
+ * One-shot fetch; useful for non-chat surfaces or static reads.
385
+ *
386
+ * 2. **Chat-surface push** — `useFileStatus({ fileId, conversationId, conversationKind? })`.
387
+ * Subscribes to the conversation channel and refreshes when
388
+ * `file_status_changed` arrives for this `fileId`. The chat service's
389
+ * media-status bridge fans photo/video lifecycle events into the per-conversation
390
+ * channel; the hook drops events for other files and dedupes against polling.
391
+ *
392
+ * 3. **Push + slow poll fallback** — combine `conversationId` with `pollIntervalMs`
393
+ * if you want belt-and-suspenders for environments where the websocket may drop.
394
+ *
395
+ * The `surface: 'profile'` push variant (private-user channel) is deferred until
396
+ * the realtime SDK exposes private-channel subscription by user.
397
+ *
398
+ * @example Chat surface (push)
375
399
  * ```tsx
376
- * function ChatImage({ fileId }: { fileId: string }) {
377
- * const { status, isReady } = useFileStatus({
378
- * fileId,
379
- * pollIntervalMs: 2000,
380
- * });
400
+ * function ChatImage({ fileId, conversationId }: { fileId: string; conversationId: string }) {
401
+ * const { status, isReady } = useFileStatus({ fileId, conversationId });
381
402
  * if (!isReady) return <div>Scanning…</div>;
382
403
  * const src = status?.urls.optimized ?? status?.urls.original;
383
404
  * return <img src={src} />;
384
405
  * }
385
406
  * ```
407
+ *
408
+ * @example Non-chat surface (pull + poll)
409
+ * ```tsx
410
+ * const { status, isReady } = useFileStatus({ fileId, pollIntervalMs: 2000 });
411
+ * ```
386
412
  */
387
413
  declare function useFileStatus(options: UseFileStatusOptions): UseFileStatusReturn;
388
414
 
@@ -411,9 +437,23 @@ interface ScaleMuleMediaProps {
411
437
  /**
412
438
  * Polling interval while waiting for the media pipeline to complete.
413
439
  * Defaults to 2000ms; set to `null` to disable polling. Polling stops
414
- * automatically once scan is clean.
440
+ * automatically once scan is clean. When `conversationId` is set the
441
+ * component receives push updates and you can usually pass `null`.
415
442
  */
416
443
  pollIntervalMs?: number | null;
444
+ /**
445
+ * Chat-surface push: when set, the underlying `useFileStatus` hook
446
+ * subscribes to the conversation's realtime channel and refreshes on
447
+ * `file_status_changed` events for this `fileId`. See `useFileStatus`
448
+ * docs for channel naming and bridge behaviour.
449
+ */
450
+ conversationId?: string | null;
451
+ /**
452
+ * Conversation kind, controls the channel name prefix
453
+ * (`conversation:{id}` vs `conversation:lr|bc|support:{id}`).
454
+ * Default is `'standard'`.
455
+ */
456
+ conversationKind?: ConversationKind;
417
457
  /**
418
458
  * Render a custom placeholder while waiting for scan / upload. Defaults
419
459
  * to a tiny "Loading…" div. Receives the current FileStatus (or null).
@@ -948,4 +988,4 @@ declare function createSafeLogger(prefix: string): {
948
988
  error: (message: string, data?: unknown) => void;
949
989
  };
950
990
 
951
- export { ApiError$1 as ApiError, 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, 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, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
991
+ export { ApiError$1 as ApiError, 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, 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, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, 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, FileStatus } from '@scalemule/sdk';
2
+ import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, 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';
@@ -164,6 +164,8 @@ interface ScaleMuleContextValue {
164
164
  photo: PhotoService;
165
165
  /** Base SDK video service — exposed for `useMedia()` and `video.uploadViaStorage()` */
166
166
  video: VideoService;
167
+ /** Base SDK audio service — exposed for `useMedia()` and `audio.uploadViaStorage()` */
168
+ audio: AudioService;
167
169
  /**
168
170
  * Default media policy for `useMedia()` calls. Set via
169
171
  * `<ScaleMuleProvider mediaPolicy="…">`; per-call overrides win.
@@ -322,25 +324,39 @@ interface UseContentOptions {
322
324
  */
323
325
  declare function useContent(options?: UseContentOptions): UseContentReturn;
324
326
 
327
+ /**
328
+ * Conversation kinds recognized by the chat realtime channel naming scheme.
329
+ * Matches `broadcast_to_conversation` in `ms/scalemule-chat/src/realtime.rs`.
330
+ */
331
+ type ConversationKind = 'standard' | 'large_room' | 'broadcast' | 'support';
325
332
  interface UseFileStatusOptions {
326
333
  /** Storage file_id to read status for. */
327
334
  fileId: string | null | undefined;
328
335
  /**
329
336
  * Optional poll interval in milliseconds. If set, the hook re-fetches
330
- * status every `pollIntervalMs`. Useful while waiting for transcode /
331
- * optimization to complete. Pass `null` (default) for a one-shot read.
332
- *
333
- * Polling stops automatically once `scan.status === 'clean'` AND the
334
- * caller's expected pipeline is done (`urls.optimized` returns 200 for
335
- * images, `urls.hls` returns 200 for videos). Today the hook can only
336
- * detect scan clean — broader pipeline-done detection lands when Phase 3
337
- * enriches the optimize/transcode response fields.
337
+ * status every `pollIntervalMs` until scan goes clean. Useful for
338
+ * non-chat surfaces or as a fallback alongside `conversationId` push.
338
339
  */
339
340
  pollIntervalMs?: number | null;
341
+ /** Disable the hook (don't fetch). Useful when `fileId` is conditional. */
342
+ disabled?: boolean;
340
343
  /**
341
- * Disable the hook (don't fetch). Useful when `fileId` is conditional.
344
+ * Chat-surface push variant: subscribe to the conversation's realtime
345
+ * channel and refresh when a `file_status_changed` event arrives for
346
+ * `fileId`. Auth rides the existing per-conversation channel ACL.
342
347
  */
343
- disabled?: boolean;
348
+ conversationId?: string | null;
349
+ /**
350
+ * Conversation kind, controls the channel name prefix:
351
+ * - `standard` (default) → `conversation:{id}`
352
+ * - `large_room` → `conversation:lr:{id}`
353
+ * - `broadcast` → `conversation:bc:{id}`
354
+ * - `support` → `conversation:support:{id}`
355
+ *
356
+ * If you only have a `messageId`, look up the conversation first via
357
+ * `client.chat.getMessage(messageId)` and pass the result here.
358
+ */
359
+ conversationKind?: ConversationKind;
344
360
  }
345
361
  interface UseFileStatusReturn {
346
362
  /** The latest status response, or null on first render / disabled. */
@@ -360,29 +376,39 @@ interface UseFileStatusReturn {
360
376
  refresh: () => Promise<void>;
361
377
  }
362
378
  /**
363
- * Subscribes to {@link FileStatus} for a single file. Today this is a
364
- * pull-only hook — single fetch by default, optional polling.
379
+ * Subscribes to {@link FileStatus} for a single file.
365
380
  *
366
- * Phase 3 of the realtime-chat media pipeline ADR will add a push variant
367
- * for chat surfaces — `useFileStatus({ messageId })` will subscribe to
368
- * `file.status` events on the existing per-conversation realtime channel
369
- * via the `scalemule-chat` translation bridge (P5'). Until that lands,
370
- * customers using this hook from chat surfaces should pass `pollIntervalMs`
371
- * in the 1–3 second range while a media pipeline is expected to be running,
372
- * then drop the polling once `isReady` is true.
381
+ * Three call shapes:
373
382
  *
374
- * @example
383
+ * 1. **Pull-only** — `useFileStatus({ fileId })` plus optional `pollIntervalMs`.
384
+ * One-shot fetch; useful for non-chat surfaces or static reads.
385
+ *
386
+ * 2. **Chat-surface push** — `useFileStatus({ fileId, conversationId, conversationKind? })`.
387
+ * Subscribes to the conversation channel and refreshes when
388
+ * `file_status_changed` arrives for this `fileId`. The chat service's
389
+ * media-status bridge fans photo/video lifecycle events into the per-conversation
390
+ * channel; the hook drops events for other files and dedupes against polling.
391
+ *
392
+ * 3. **Push + slow poll fallback** — combine `conversationId` with `pollIntervalMs`
393
+ * if you want belt-and-suspenders for environments where the websocket may drop.
394
+ *
395
+ * The `surface: 'profile'` push variant (private-user channel) is deferred until
396
+ * the realtime SDK exposes private-channel subscription by user.
397
+ *
398
+ * @example Chat surface (push)
375
399
  * ```tsx
376
- * function ChatImage({ fileId }: { fileId: string }) {
377
- * const { status, isReady } = useFileStatus({
378
- * fileId,
379
- * pollIntervalMs: 2000,
380
- * });
400
+ * function ChatImage({ fileId, conversationId }: { fileId: string; conversationId: string }) {
401
+ * const { status, isReady } = useFileStatus({ fileId, conversationId });
381
402
  * if (!isReady) return <div>Scanning…</div>;
382
403
  * const src = status?.urls.optimized ?? status?.urls.original;
383
404
  * return <img src={src} />;
384
405
  * }
385
406
  * ```
407
+ *
408
+ * @example Non-chat surface (pull + poll)
409
+ * ```tsx
410
+ * const { status, isReady } = useFileStatus({ fileId, pollIntervalMs: 2000 });
411
+ * ```
386
412
  */
387
413
  declare function useFileStatus(options: UseFileStatusOptions): UseFileStatusReturn;
388
414
 
@@ -411,9 +437,23 @@ interface ScaleMuleMediaProps {
411
437
  /**
412
438
  * Polling interval while waiting for the media pipeline to complete.
413
439
  * Defaults to 2000ms; set to `null` to disable polling. Polling stops
414
- * automatically once scan is clean.
440
+ * automatically once scan is clean. When `conversationId` is set the
441
+ * component receives push updates and you can usually pass `null`.
415
442
  */
416
443
  pollIntervalMs?: number | null;
444
+ /**
445
+ * Chat-surface push: when set, the underlying `useFileStatus` hook
446
+ * subscribes to the conversation's realtime channel and refreshes on
447
+ * `file_status_changed` events for this `fileId`. See `useFileStatus`
448
+ * docs for channel naming and bridge behaviour.
449
+ */
450
+ conversationId?: string | null;
451
+ /**
452
+ * Conversation kind, controls the channel name prefix
453
+ * (`conversation:{id}` vs `conversation:lr|bc|support:{id}`).
454
+ * Default is `'standard'`.
455
+ */
456
+ conversationKind?: ConversationKind;
417
457
  /**
418
458
  * Render a custom placeholder while waiting for scan / upload. Defaults
419
459
  * to a tiny "Loading…" div. Receives the current FileStatus (or null).
@@ -948,4 +988,4 @@ declare function createSafeLogger(prefix: string): {
948
988
  error: (message: string, data?: unknown) => void;
949
989
  };
950
990
 
951
- export { ApiError$1 as ApiError, 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, 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, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
991
+ export { ApiError$1 as ApiError, 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, 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, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
package/dist/index.js CHANGED
@@ -2727,6 +2727,25 @@ var StorageService = class extends ServiceModule {
2727
2727
  async getInfo(fileId, options) {
2728
2728
  return this._get(`/files/${fileId}/info`, options);
2729
2729
  }
2730
+ /**
2731
+ * Get the calling app's storage + media settings — content policy,
2732
+ * retention, max upload size, and the per-app `media_policy`.
2733
+ *
2734
+ * `media_policy` drives release-gating in the SDK upload helpers:
2735
+ * `fast_trusted` / `safe_visible` resolve immediately on upload; the
2736
+ * `safe_public` / `moderated` / `compliance` modes await pipeline
2737
+ * completion before resolving the upload promise.
2738
+ */
2739
+ async getSettings(options) {
2740
+ return this._get("/settings", options);
2741
+ }
2742
+ /**
2743
+ * Update the calling app's storage + media settings. Admin-only on the
2744
+ * platform side (callers without the right role get 403).
2745
+ */
2746
+ async updateSettings(settings, options) {
2747
+ return this.put("/settings", settings, options);
2748
+ }
2730
2749
  /**
2731
2750
  * Aggregate status for a file — single call returns scan + reserved
2732
2751
  * optimize/transcode slots + the canonical view URL paths for image
@@ -5678,6 +5697,72 @@ var PhotoService = class extends ServiceModule {
5678
5697
  return this.get(id);
5679
5698
  }
5680
5699
  };
5700
+ var AudioService = class extends ServiceModule {
5701
+ /**
5702
+ * @param storage Required for {@link uploadViaStorage}. Wired up by the
5703
+ * top-level {@link ScaleMule} constructor — most call sites should not
5704
+ * instantiate `AudioService` directly.
5705
+ */
5706
+ constructor(client, storage) {
5707
+ super(client);
5708
+ this.storage = storage;
5709
+ this.basePath = "/v1/audios";
5710
+ }
5711
+ /**
5712
+ * Register a storage-uploaded audio asset with the audio service.
5713
+ * Idempotent — re-calling with the same `file_id` returns the existing
5714
+ * record.
5715
+ */
5716
+ async register(args, options) {
5717
+ return this.post("/register", { file_id: args.fileId, sm_user_id: args.userId }, options);
5718
+ }
5719
+ /**
5720
+ * Upload a file to storage (browser → S3 direct, private, uncompressed)
5721
+ * and register it with the audio service.
5722
+ *
5723
+ * Mirrors `photo.uploadViaStorage()` / `video.uploadViaStorage()`. If
5724
+ * `register()` fails after a successful storage upload, the file is *not*
5725
+ * lost — the returned `file_id` is still valid as a generic private storage
5726
+ * file. The SDK logs a warning and returns `audio_id: null`.
5727
+ */
5728
+ async uploadViaStorage(file, uploadOptions, requestOptions) {
5729
+ const uploadResult = await this.storage.uploadPrivate(file, {
5730
+ filename: uploadOptions?.filename,
5731
+ metadata: uploadOptions?.metadata,
5732
+ onProgress: uploadOptions?.onProgress,
5733
+ signal: uploadOptions?.signal
5734
+ });
5735
+ if (uploadResult.error || !uploadResult.data) {
5736
+ return { data: null, error: uploadResult.error };
5737
+ }
5738
+ const fileInfo = uploadResult.data;
5739
+ const fileId = fileInfo.id;
5740
+ const originalViewUrl = fileInfo.url ?? null;
5741
+ const registerResult = await this.register({ fileId, userId: uploadOptions?.userId }, requestOptions);
5742
+ if (registerResult.error || !registerResult.data) {
5743
+ console.warn(
5744
+ "[scalemule-sdk] audio.register() failed after storage upload; typed audio metadata unavailable.",
5745
+ registerResult.error
5746
+ );
5747
+ return {
5748
+ data: {
5749
+ file_id: fileId,
5750
+ audio_id: null,
5751
+ original_view_url: originalViewUrl
5752
+ },
5753
+ error: null
5754
+ };
5755
+ }
5756
+ return {
5757
+ data: {
5758
+ file_id: fileId,
5759
+ audio_id: registerResult.data.audio_id,
5760
+ original_view_url: originalViewUrl
5761
+ },
5762
+ error: null
5763
+ };
5764
+ }
5765
+ };
5681
5766
  var DeadLetterApi = class extends ServiceModule {
5682
5767
  constructor() {
5683
5768
  super(...arguments);
@@ -6497,6 +6582,7 @@ var ScaleMule = class {
6497
6582
  this.graph = new GraphService(this._client);
6498
6583
  this.functions = new FunctionsService(this._client);
6499
6584
  this.photo = new PhotoService(this._client, this.storage);
6585
+ this.audio = new AudioService(this._client, this.storage);
6500
6586
  this.flagContent = new FlagContentService(this._client);
6501
6587
  this.creatorMaker = new CreatorMakerService(this._client);
6502
6588
  this.compliance = new ComplianceService(this._client);
@@ -7585,6 +7671,7 @@ function ScaleMuleProvider({
7585
7671
  storage: baseClient.storage,
7586
7672
  photo: baseClient.photo,
7587
7673
  video: baseClient.video,
7674
+ audio: baseClient.audio,
7588
7675
  mediaPolicy,
7589
7676
  user,
7590
7677
  setUser: handleSetUser,
@@ -8899,7 +8986,7 @@ function useContent(options = {}) {
8899
8986
  );
8900
8987
  }
8901
8988
  function useMedia() {
8902
- const { storage, photo, video, mediaPolicy: providerDefaultPolicy } = useScaleMule();
8989
+ const { storage, photo, video, audio, mediaPolicy: providerDefaultPolicy } = useScaleMule();
8903
8990
  const [uploading, setUploading] = React.useState(false);
8904
8991
  const [error, setError] = React.useState(null);
8905
8992
  const upload = React.useCallback(
@@ -8953,6 +9040,21 @@ function useMedia() {
8953
9040
  is_public: false
8954
9041
  };
8955
9042
  }
9043
+ if (mimeType.startsWith("audio/") && !isPublic) {
9044
+ const r2 = await audio.uploadViaStorage(file, sharedOpts);
9045
+ if (r2.error || !r2.data) {
9046
+ throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9047
+ }
9048
+ return {
9049
+ file_id: r2.data.file_id,
9050
+ photo_id: null,
9051
+ original_view_url: r2.data.original_view_url,
9052
+ optimized_url_promise: Promise.resolve(null),
9053
+ hls_url_promise: Promise.resolve(null),
9054
+ mime_type: mimeType,
9055
+ is_public: false
9056
+ };
9057
+ }
8956
9058
  if (mimeType.startsWith("image/") && isPublic) {
8957
9059
  const r2 = await storage.upload(file, {
8958
9060
  ...sharedOpts,
@@ -8995,7 +9097,7 @@ function useMedia() {
8995
9097
  setUploading(false);
8996
9098
  }
8997
9099
  },
8998
- [storage, photo, video, providerDefaultPolicy]
9100
+ [storage, photo, video, audio, providerDefaultPolicy]
8999
9101
  );
9000
9102
  const cancelUpload = React.useCallback(
9001
9103
  async (fileId) => {
@@ -9016,9 +9118,28 @@ function useMedia() {
9016
9118
  );
9017
9119
  return { upload, cancelUpload, error, uploading };
9018
9120
  }
9121
+ function conversationChannel(kind, id) {
9122
+ switch (kind) {
9123
+ case "large_room":
9124
+ return `conversation:lr:${id}`;
9125
+ case "broadcast":
9126
+ return `conversation:bc:${id}`;
9127
+ case "support":
9128
+ return `conversation:support:${id}`;
9129
+ case "standard":
9130
+ default:
9131
+ return `conversation:${id}`;
9132
+ }
9133
+ }
9019
9134
  function useFileStatus(options) {
9020
- const { storage } = useScaleMule();
9021
- const { fileId, pollIntervalMs = null, disabled = false } = options;
9135
+ const { storage, realtime } = useScaleMule();
9136
+ const {
9137
+ fileId,
9138
+ pollIntervalMs = null,
9139
+ disabled = false,
9140
+ conversationId = null,
9141
+ conversationKind = "standard"
9142
+ } = options;
9022
9143
  const [status, setStatus] = React.useState(null);
9023
9144
  const [loading, setLoading] = React.useState(false);
9024
9145
  const [error, setError] = React.useState(null);
@@ -9057,6 +9178,20 @@ function useFileStatus(options) {
9057
9178
  }, pollIntervalMs);
9058
9179
  return () => clearInterval(id);
9059
9180
  }, [pollIntervalMs, disabled, fileId, status?.scan.status, fetchStatus]);
9181
+ React.useEffect(() => {
9182
+ if (!conversationId || !fileId || disabled) return;
9183
+ const channel = conversationChannel(conversationKind, conversationId);
9184
+ const unsub = realtime.subscribe(channel, (data) => {
9185
+ if (typeof data !== "object" || data === null) return;
9186
+ const payload = data;
9187
+ const wrapped = "data" in payload && typeof payload.data === "object" && payload.data !== null ? payload.data : payload;
9188
+ const evt = typeof payload.event === "string" ? payload.event : void 0;
9189
+ if (evt && evt !== "file_status_changed") return;
9190
+ if (wrapped.file_id !== fileId) return;
9191
+ void fetchStatus();
9192
+ });
9193
+ return unsub;
9194
+ }, [realtime, conversationId, conversationKind, fileId, disabled, fetchStatus]);
9060
9195
  const isReady = status?.scan.status === "clean";
9061
9196
  return { status, loading, error, isReady, refresh: fetchStatus };
9062
9197
  }
@@ -9071,11 +9206,18 @@ function ScaleMuleMedia(props) {
9071
9206
  style,
9072
9207
  alt,
9073
9208
  pollIntervalMs = 2e3,
9209
+ conversationId = null,
9210
+ conversationKind,
9074
9211
  renderPlaceholder,
9075
9212
  renderBlocked,
9076
9213
  renderOverride
9077
9214
  } = props;
9078
- const { status, isReady } = useFileStatus({ fileId, pollIntervalMs });
9215
+ const { status, isReady } = useFileStatus({
9216
+ fileId,
9217
+ pollIntervalMs,
9218
+ conversationId,
9219
+ conversationKind
9220
+ });
9079
9221
  const isImage = mimeType.startsWith("image/");
9080
9222
  const isVideo = mimeType.startsWith("video/");
9081
9223
  const isAudio = mimeType.startsWith("audio/");
package/dist/index.mjs CHANGED
@@ -2707,6 +2707,25 @@ var StorageService = class extends ServiceModule {
2707
2707
  async getInfo(fileId, options) {
2708
2708
  return this._get(`/files/${fileId}/info`, options);
2709
2709
  }
2710
+ /**
2711
+ * Get the calling app's storage + media settings — content policy,
2712
+ * retention, max upload size, and the per-app `media_policy`.
2713
+ *
2714
+ * `media_policy` drives release-gating in the SDK upload helpers:
2715
+ * `fast_trusted` / `safe_visible` resolve immediately on upload; the
2716
+ * `safe_public` / `moderated` / `compliance` modes await pipeline
2717
+ * completion before resolving the upload promise.
2718
+ */
2719
+ async getSettings(options) {
2720
+ return this._get("/settings", options);
2721
+ }
2722
+ /**
2723
+ * Update the calling app's storage + media settings. Admin-only on the
2724
+ * platform side (callers without the right role get 403).
2725
+ */
2726
+ async updateSettings(settings, options) {
2727
+ return this.put("/settings", settings, options);
2728
+ }
2710
2729
  /**
2711
2730
  * Aggregate status for a file — single call returns scan + reserved
2712
2731
  * optimize/transcode slots + the canonical view URL paths for image
@@ -5658,6 +5677,72 @@ var PhotoService = class extends ServiceModule {
5658
5677
  return this.get(id);
5659
5678
  }
5660
5679
  };
5680
+ var AudioService = class extends ServiceModule {
5681
+ /**
5682
+ * @param storage Required for {@link uploadViaStorage}. Wired up by the
5683
+ * top-level {@link ScaleMule} constructor — most call sites should not
5684
+ * instantiate `AudioService` directly.
5685
+ */
5686
+ constructor(client, storage) {
5687
+ super(client);
5688
+ this.storage = storage;
5689
+ this.basePath = "/v1/audios";
5690
+ }
5691
+ /**
5692
+ * Register a storage-uploaded audio asset with the audio service.
5693
+ * Idempotent — re-calling with the same `file_id` returns the existing
5694
+ * record.
5695
+ */
5696
+ async register(args, options) {
5697
+ return this.post("/register", { file_id: args.fileId, sm_user_id: args.userId }, options);
5698
+ }
5699
+ /**
5700
+ * Upload a file to storage (browser → S3 direct, private, uncompressed)
5701
+ * and register it with the audio service.
5702
+ *
5703
+ * Mirrors `photo.uploadViaStorage()` / `video.uploadViaStorage()`. If
5704
+ * `register()` fails after a successful storage upload, the file is *not*
5705
+ * lost — the returned `file_id` is still valid as a generic private storage
5706
+ * file. The SDK logs a warning and returns `audio_id: null`.
5707
+ */
5708
+ async uploadViaStorage(file, uploadOptions, requestOptions) {
5709
+ const uploadResult = await this.storage.uploadPrivate(file, {
5710
+ filename: uploadOptions?.filename,
5711
+ metadata: uploadOptions?.metadata,
5712
+ onProgress: uploadOptions?.onProgress,
5713
+ signal: uploadOptions?.signal
5714
+ });
5715
+ if (uploadResult.error || !uploadResult.data) {
5716
+ return { data: null, error: uploadResult.error };
5717
+ }
5718
+ const fileInfo = uploadResult.data;
5719
+ const fileId = fileInfo.id;
5720
+ const originalViewUrl = fileInfo.url ?? null;
5721
+ const registerResult = await this.register({ fileId, userId: uploadOptions?.userId }, requestOptions);
5722
+ if (registerResult.error || !registerResult.data) {
5723
+ console.warn(
5724
+ "[scalemule-sdk] audio.register() failed after storage upload; typed audio metadata unavailable.",
5725
+ registerResult.error
5726
+ );
5727
+ return {
5728
+ data: {
5729
+ file_id: fileId,
5730
+ audio_id: null,
5731
+ original_view_url: originalViewUrl
5732
+ },
5733
+ error: null
5734
+ };
5735
+ }
5736
+ return {
5737
+ data: {
5738
+ file_id: fileId,
5739
+ audio_id: registerResult.data.audio_id,
5740
+ original_view_url: originalViewUrl
5741
+ },
5742
+ error: null
5743
+ };
5744
+ }
5745
+ };
5661
5746
  var DeadLetterApi = class extends ServiceModule {
5662
5747
  constructor() {
5663
5748
  super(...arguments);
@@ -6477,6 +6562,7 @@ var ScaleMule = class {
6477
6562
  this.graph = new GraphService(this._client);
6478
6563
  this.functions = new FunctionsService(this._client);
6479
6564
  this.photo = new PhotoService(this._client, this.storage);
6565
+ this.audio = new AudioService(this._client, this.storage);
6480
6566
  this.flagContent = new FlagContentService(this._client);
6481
6567
  this.creatorMaker = new CreatorMakerService(this._client);
6482
6568
  this.compliance = new ComplianceService(this._client);
@@ -7565,6 +7651,7 @@ function ScaleMuleProvider({
7565
7651
  storage: baseClient.storage,
7566
7652
  photo: baseClient.photo,
7567
7653
  video: baseClient.video,
7654
+ audio: baseClient.audio,
7568
7655
  mediaPolicy,
7569
7656
  user,
7570
7657
  setUser: handleSetUser,
@@ -8879,7 +8966,7 @@ function useContent(options = {}) {
8879
8966
  );
8880
8967
  }
8881
8968
  function useMedia() {
8882
- const { storage, photo, video, mediaPolicy: providerDefaultPolicy } = useScaleMule();
8969
+ const { storage, photo, video, audio, mediaPolicy: providerDefaultPolicy } = useScaleMule();
8883
8970
  const [uploading, setUploading] = useState(false);
8884
8971
  const [error, setError] = useState(null);
8885
8972
  const upload = useCallback(
@@ -8933,6 +9020,21 @@ function useMedia() {
8933
9020
  is_public: false
8934
9021
  };
8935
9022
  }
9023
+ if (mimeType.startsWith("audio/") && !isPublic) {
9024
+ const r2 = await audio.uploadViaStorage(file, sharedOpts);
9025
+ if (r2.error || !r2.data) {
9026
+ throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9027
+ }
9028
+ return {
9029
+ file_id: r2.data.file_id,
9030
+ photo_id: null,
9031
+ original_view_url: r2.data.original_view_url,
9032
+ optimized_url_promise: Promise.resolve(null),
9033
+ hls_url_promise: Promise.resolve(null),
9034
+ mime_type: mimeType,
9035
+ is_public: false
9036
+ };
9037
+ }
8936
9038
  if (mimeType.startsWith("image/") && isPublic) {
8937
9039
  const r2 = await storage.upload(file, {
8938
9040
  ...sharedOpts,
@@ -8975,7 +9077,7 @@ function useMedia() {
8975
9077
  setUploading(false);
8976
9078
  }
8977
9079
  },
8978
- [storage, photo, video, providerDefaultPolicy]
9080
+ [storage, photo, video, audio, providerDefaultPolicy]
8979
9081
  );
8980
9082
  const cancelUpload = useCallback(
8981
9083
  async (fileId) => {
@@ -8996,9 +9098,28 @@ function useMedia() {
8996
9098
  );
8997
9099
  return { upload, cancelUpload, error, uploading };
8998
9100
  }
9101
+ function conversationChannel(kind, id) {
9102
+ switch (kind) {
9103
+ case "large_room":
9104
+ return `conversation:lr:${id}`;
9105
+ case "broadcast":
9106
+ return `conversation:bc:${id}`;
9107
+ case "support":
9108
+ return `conversation:support:${id}`;
9109
+ case "standard":
9110
+ default:
9111
+ return `conversation:${id}`;
9112
+ }
9113
+ }
8999
9114
  function useFileStatus(options) {
9000
- const { storage } = useScaleMule();
9001
- const { fileId, pollIntervalMs = null, disabled = false } = options;
9115
+ const { storage, realtime } = useScaleMule();
9116
+ const {
9117
+ fileId,
9118
+ pollIntervalMs = null,
9119
+ disabled = false,
9120
+ conversationId = null,
9121
+ conversationKind = "standard"
9122
+ } = options;
9002
9123
  const [status, setStatus] = useState(null);
9003
9124
  const [loading, setLoading] = useState(false);
9004
9125
  const [error, setError] = useState(null);
@@ -9037,6 +9158,20 @@ function useFileStatus(options) {
9037
9158
  }, pollIntervalMs);
9038
9159
  return () => clearInterval(id);
9039
9160
  }, [pollIntervalMs, disabled, fileId, status?.scan.status, fetchStatus]);
9161
+ useEffect(() => {
9162
+ if (!conversationId || !fileId || disabled) return;
9163
+ const channel = conversationChannel(conversationKind, conversationId);
9164
+ const unsub = realtime.subscribe(channel, (data) => {
9165
+ if (typeof data !== "object" || data === null) return;
9166
+ const payload = data;
9167
+ const wrapped = "data" in payload && typeof payload.data === "object" && payload.data !== null ? payload.data : payload;
9168
+ const evt = typeof payload.event === "string" ? payload.event : void 0;
9169
+ if (evt && evt !== "file_status_changed") return;
9170
+ if (wrapped.file_id !== fileId) return;
9171
+ void fetchStatus();
9172
+ });
9173
+ return unsub;
9174
+ }, [realtime, conversationId, conversationKind, fileId, disabled, fetchStatus]);
9040
9175
  const isReady = status?.scan.status === "clean";
9041
9176
  return { status, loading, error, isReady, refresh: fetchStatus };
9042
9177
  }
@@ -9051,11 +9186,18 @@ function ScaleMuleMedia(props) {
9051
9186
  style,
9052
9187
  alt,
9053
9188
  pollIntervalMs = 2e3,
9189
+ conversationId = null,
9190
+ conversationKind,
9054
9191
  renderPlaceholder,
9055
9192
  renderBlocked,
9056
9193
  renderOverride
9057
9194
  } = props;
9058
- const { status, isReady } = useFileStatus({ fileId, pollIntervalMs });
9195
+ const { status, isReady } = useFileStatus({
9196
+ fileId,
9197
+ pollIntervalMs,
9198
+ conversationId,
9199
+ conversationKind
9200
+ });
9059
9201
  const isImage = mimeType.startsWith("image/");
9060
9202
  const isVideo = mimeType.startsWith("video/");
9061
9203
  const isAudio = mimeType.startsWith("audio/");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/nextjs",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "ScaleMule SDK for Next.js applications - authentication, storage, and user management",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@scalemule/money": "^0.0.1",
57
- "@scalemule/sdk": "^0.0.41"
57
+ "@scalemule/sdk": "^0.0.43"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "next": ">=14.0.0",