@scalemule/nextjs 0.1.27 → 0.1.28

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.d.mts CHANGED
@@ -219,6 +219,8 @@ interface ScaleMuleContextValue {
219
219
  video: VideoService;
220
220
  /** Base SDK audio service — exposed for `useMedia()` and `audio.uploadViaStorage()` */
221
221
  audio: AudioService;
222
+ /** Base SDK media facade — shared upload/list/delete surface across apps and scripts. */
223
+ media: unknown;
222
224
  /** Base SDK TTS service — exposed for `useTtsJob()` and direct narration requests */
223
225
  tts: TtsService;
224
226
  /**
package/dist/index.d.ts CHANGED
@@ -219,6 +219,8 @@ interface ScaleMuleContextValue {
219
219
  video: VideoService;
220
220
  /** Base SDK audio service — exposed for `useMedia()` and `audio.uploadViaStorage()` */
221
221
  audio: AudioService;
222
+ /** Base SDK media facade — shared upload/list/delete surface across apps and scripts. */
223
+ media: unknown;
222
224
  /** Base SDK TTS service — exposed for `useTtsJob()` and direct narration requests */
223
225
  tts: TtsService;
224
226
  /**
package/dist/index.js CHANGED
@@ -2113,6 +2113,24 @@ var DEFAULT_STALL_TIMEOUT_MS = 45e3;
2113
2113
  var SLOW_NETWORK_STALL_TIMEOUT_MS = 9e4;
2114
2114
  var MULTIPART_THRESHOLD = 8 * 1024 * 1024;
2115
2115
  var MULTIPART_THRESHOLD_SLOW = 4 * 1024 * 1024;
2116
+ function buildVisibilityWire(options) {
2117
+ if (!options) return {};
2118
+ if (options.visibility) {
2119
+ return {
2120
+ visibility: options.visibility,
2121
+ // Legacy field for old storage instances that haven't deployed
2122
+ // the tristate handler yet — omit `is_public` for the
2123
+ // `anonymous_visible` case so an old server that doesn't know
2124
+ // the new value at least falls through to its own default
2125
+ // rather than mis-treating it as `app_public`.
2126
+ ...options.visibility === "private" ? { is_public: false } : options.visibility === "app_public" ? { is_public: true } : {}
2127
+ };
2128
+ }
2129
+ if (typeof options.isPublic === "boolean") {
2130
+ return { is_public: options.isPublic };
2131
+ }
2132
+ return {};
2133
+ }
2116
2134
  var StorageService = class extends ServiceModule {
2117
2135
  constructor() {
2118
2136
  super(...arguments);
@@ -2194,6 +2212,42 @@ var StorageService = class extends ServiceModule {
2194
2212
  }
2195
2213
  return result;
2196
2214
  }
2215
+ /**
2216
+ * Upload a file as world-readable on the anonymous public CDN.
2217
+ *
2218
+ * Same browser→S3 pipeline as {@link upload}, but pins
2219
+ * `visibility: 'anonymous_visible'` — caller cannot opt out.
2220
+ *
2221
+ * Use this for media meant to render on logged-out marketing
2222
+ * pages, blog posts, embed snippets — anywhere a customer needs
2223
+ * to drop a URL into `<img src>` without an authenticated
2224
+ * session. For everything else (chat attachments, private
2225
+ * uploads, app-internal galleries) prefer {@link upload} or
2226
+ * {@link uploadPrivate}.
2227
+ *
2228
+ * **`cdn_url` may be `null` on the immediate response.** Storage
2229
+ * intentionally withholds the public CDN URL until the AV scan
2230
+ * has flipped to `clean` — exposing the URL pre-scan would let a
2231
+ * customer embed the bytes in a logged-out page before the
2232
+ * platform has verified them. The upload itself succeeds
2233
+ * regardless; consumers should check `result.data.cdn_url` and:
2234
+ * - if non-null → it's safe to publish
2235
+ * - if null → poll {@link getFileStatus} on a small backoff
2236
+ * until `urls.cdn_url` populates (or `scan.status` flips to
2237
+ * `threat` / `quarantined` / `error` — terminal failure)
2238
+ *
2239
+ * Requires the operator to have provisioned the anonymous
2240
+ * delivery bucket + CDN. When they haven't, the storage service
2241
+ * returns 503 `ANONYMOUS_DELIVERY_NOT_CONFIGURED` (an error
2242
+ * propagated through this helper) — it never silently demotes
2243
+ * to `app_public`.
2244
+ */
2245
+ async uploadAnonymous(file, options) {
2246
+ return this.upload(file, {
2247
+ ...options,
2248
+ visibility: "anonymous_visible"
2249
+ });
2250
+ }
2197
2251
  // --------------------------------------------------------------------------
2198
2252
  // Direct Upload (3-step with retry + stall)
2199
2253
  // --------------------------------------------------------------------------
@@ -2207,7 +2261,7 @@ var StorageService = class extends ServiceModule {
2207
2261
  filename,
2208
2262
  content_type: file.type || "application/octet-stream",
2209
2263
  size_bytes: file.size,
2210
- is_public: options?.isPublic ?? true,
2264
+ ...buildVisibilityWire(options),
2211
2265
  metadata: options?.metadata
2212
2266
  },
2213
2267
  requestOpts
@@ -2298,6 +2352,13 @@ var StorageService = class extends ServiceModule {
2298
2352
  content_type: d.content_type,
2299
2353
  size_bytes: d.size_bytes,
2300
2354
  url: d.url,
2355
+ // Surface visibility + cdn_url all the way out so callers
2356
+ // can branch on visibility without a follow-up getInfo()
2357
+ // round-trip. Anonymous-visible callers especially need
2358
+ // `cdn_url` here to render the image immediately.
2359
+ visibility: d.visibility,
2360
+ cdn_url: d.cdn_url,
2361
+ is_public: d.visibility ? d.visibility !== "private" : void 0,
2301
2362
  created_at: (/* @__PURE__ */ new Date()).toISOString()
2302
2363
  },
2303
2364
  error: null
@@ -2379,7 +2440,7 @@ var StorageService = class extends ServiceModule {
2379
2440
  filename,
2380
2441
  content_type: file.type || "application/octet-stream",
2381
2442
  size_bytes: file.size,
2382
- is_public: options?.isPublic ?? true,
2443
+ ...buildVisibilityWire(options),
2383
2444
  metadata: options?.metadata,
2384
2445
  chunk_size: options?.chunkSize,
2385
2446
  ...clientUploadKey ? { client_upload_key: clientUploadKey } : {}
@@ -2657,6 +2718,9 @@ var StorageService = class extends ServiceModule {
2657
2718
  content_type: d.content_type,
2658
2719
  size_bytes: d.size_bytes,
2659
2720
  url: d.url,
2721
+ visibility: d.visibility,
2722
+ cdn_url: d.cdn_url,
2723
+ is_public: d.visibility ? d.visibility !== "private" : void 0,
2660
2724
  created_at: (/* @__PURE__ */ new Date()).toISOString()
2661
2725
  },
2662
2726
  error: null
@@ -2694,7 +2758,7 @@ var StorageService = class extends ServiceModule {
2694
2758
  {
2695
2759
  filename,
2696
2760
  content_type: contentType,
2697
- is_public: options?.isPublic ?? true,
2761
+ ...buildVisibilityWire(options),
2698
2762
  expires_in: options?.expiresIn ?? 3600,
2699
2763
  size_bytes: options?.sizeBytes,
2700
2764
  metadata: options?.metadata
@@ -2741,7 +2805,16 @@ var StorageService = class extends ServiceModule {
2741
2805
  // --------------------------------------------------------------------------
2742
2806
  // Multipart Public API (for advanced/server-side usage)
2743
2807
  // --------------------------------------------------------------------------
2744
- /** Start a multipart upload session. */
2808
+ /**
2809
+ * Start a multipart upload session.
2810
+ *
2811
+ * Accepts both the typed `visibility` field (preferred) and the
2812
+ * legacy `is_public` boolean for back-compat. If both are sent the
2813
+ * storage service uses `visibility`. `'anonymous_visible'` requires
2814
+ * the operator to have provisioned the anonymous bucket — the
2815
+ * service returns 503 `ANONYMOUS_DELIVERY_NOT_CONFIGURED`
2816
+ * otherwise (the SDK never silently demotes).
2817
+ */
2745
2818
  async startMultipartUpload(params, requestOptions) {
2746
2819
  return this.post("/signed-url/multipart/start", params, requestOptions);
2747
2820
  }
@@ -2872,14 +2945,26 @@ var StorageService = class extends ServiceModule {
2872
2945
  return this._get(`/files/${fileId}/view-status`, options);
2873
2946
  }
2874
2947
  /**
2875
- * Update a file's visibility (public/private).
2876
- * Only the file owner can toggle this. Changes URL TTL — does not move the S3 object.
2877
- * Public files get 7-day signed URLs; private files get 1-hour signed URLs.
2948
+ * Update a file's visibility.
2949
+ *
2950
+ * Accepts either:
2951
+ * - a tri-state {@link Visibility} string (preferred), or
2952
+ * - a legacy boolean (`true` → `app_public`, `false` → `private`).
2953
+ *
2954
+ * Only the file owner / app member can toggle this. Same-bucket
2955
+ * transitions (`private` ↔ `app_public`) succeed and the service
2956
+ * keeps the legacy `is_public` column in sync via a DB trigger.
2957
+ * Cross-bucket transitions (any flip into or out of
2958
+ * `'anonymous_visible'`) currently return 409
2959
+ * `VISIBILITY_TRANSITION_UNSUPPORTED` — the bytes would need to
2960
+ * move between S3 buckets and that orchestration is not yet
2961
+ * shipped. Re-upload the file with the desired visibility instead.
2878
2962
  */
2879
- async updateVisibility(fileId, isPublic, options) {
2963
+ async updateVisibility(fileId, visibility, options) {
2964
+ const body = typeof visibility === "boolean" ? { is_public: visibility } : { visibility };
2880
2965
  return this.patch(
2881
2966
  `/files/${fileId}/visibility`,
2882
- { is_public: isPublic },
2967
+ body,
2883
2968
  options
2884
2969
  );
2885
2970
  }
@@ -2977,6 +3062,7 @@ var StorageService = class extends ServiceModule {
2977
3062
  clearTimeout(timer);
2978
3063
  parentSignalCleanup?.();
2979
3064
  if (!response.ok) {
3065
+ const s3ErrorBody = await response.text().catch(() => "");
2980
3066
  return {
2981
3067
  data: null,
2982
3068
  error: {
@@ -2986,7 +3072,9 @@ var StorageService = class extends ServiceModule {
2986
3072
  details: {
2987
3073
  transport: "fetch",
2988
3074
  total_bytes: file.size,
2989
- online: getOnlineStatus()
3075
+ online: getOnlineStatus(),
3076
+ s3_error_body: s3ErrorBody.slice(0, 1024),
3077
+ s3_error_code: extractS3ErrorCode(s3ErrorBody)
2990
3078
  }
2991
3079
  }
2992
3080
  };
@@ -3077,6 +3165,7 @@ var StorageService = class extends ServiceModule {
3077
3165
  onProgress?.(100);
3078
3166
  resolve({ data: null, error: null });
3079
3167
  } else {
3168
+ const body = (xhr.responseText || "").slice(0, 1024);
3080
3169
  resolve({
3081
3170
  data: null,
3082
3171
  error: {
@@ -3088,7 +3177,9 @@ var StorageService = class extends ServiceModule {
3088
3177
  bytes_sent: lastLoaded,
3089
3178
  total_bytes: totalBytes,
3090
3179
  progress_percent: totalBytes > 0 ? Math.round(lastLoaded / totalBytes * 100) : void 0,
3091
- online: getOnlineStatus()
3180
+ online: getOnlineStatus(),
3181
+ s3_error_body: body,
3182
+ s3_error_code: extractS3ErrorCode(body)
3092
3183
  }
3093
3184
  }
3094
3185
  });
@@ -3167,12 +3258,18 @@ var StorageService = class extends ServiceModule {
3167
3258
  }
3168
3259
  return { etag };
3169
3260
  }
3261
+ const body = await response.text().catch(() => "");
3262
+ const s3ErrorCode = extractS3ErrorCode(body);
3170
3263
  if (NON_RETRYABLE_STATUS_CODES.has(response.status)) {
3171
- return { error: `Part upload failed: ${response.status}`, status: response.status, code: "s3_error" };
3264
+ return {
3265
+ error: `Part upload failed: ${response.status}${s3ErrorCode ? ` (${s3ErrorCode})` : ""}`,
3266
+ status: response.status,
3267
+ code: response.status === 403 ? "s3_signature_error" : "s3_error"
3268
+ };
3172
3269
  }
3173
3270
  if (attempt === RETRY_DELAYS.length - 1) {
3174
3271
  return {
3175
- error: `Part upload failed after retries: ${response.status}`,
3272
+ error: `Part upload failed after retries: ${response.status}${s3ErrorCode ? ` (${s3ErrorCode})` : ""}`,
3176
3273
  status: response.status,
3177
3274
  code: "s3_error"
3178
3275
  };
@@ -3298,6 +3395,11 @@ function getUploadEnvironmentDiagnostics() {
3298
3395
  function asNumber2(value) {
3299
3396
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3300
3397
  }
3398
+ function extractS3ErrorCode(body) {
3399
+ if (!body) return void 0;
3400
+ const m = body.match(/<Code>([^<]+)<\/Code>/);
3401
+ return m ? m[1] : void 0;
3402
+ }
3301
3403
  var DEFAULT_RECONNECT_BASE_MS = 1e3;
3302
3404
  var MAX_RECONNECT_MS = 3e4;
3303
3405
  var HEARTBEAT_INTERVAL_MS = 3e4;
@@ -4211,6 +4313,56 @@ var SocialService = class extends ServiceModule {
4211
4313
  return this._list(`/${targetType}/${targetId}/likes`, params, requestOptions);
4212
4314
  }
4213
4315
  // --------------------------------------------------------------------------
4316
+ // Voting (bipolar — up/down with score)
4317
+ // --------------------------------------------------------------------------
4318
+ /**
4319
+ * Cast, change, or clear the caller's vote on a target.
4320
+ *
4321
+ * `value`: 1 = upvote, -1 = downvote, 0 = clear.
4322
+ *
4323
+ * Idempotent: re-casting the same value is a no-op; clearing when no
4324
+ * vote exists is a no-op. Server validates `target_type` matches
4325
+ * `[a-z0-9_]{1,64}`. Convention: prefix per app (`weekmob_post`,
4326
+ * `gistyo_gist`).
4327
+ *
4328
+ * Requires an authenticated session; anonymous calls return 401.
4329
+ */
4330
+ async vote(targetType, targetId, value, options) {
4331
+ return this.put(`/${targetType}/${targetId}/vote`, { value }, options);
4332
+ }
4333
+ /**
4334
+ * Read the caller's current vote on a target plus the aggregate counts.
4335
+ * `value` is 0 when the caller has no vote.
4336
+ *
4337
+ * Requires an authenticated session.
4338
+ */
4339
+ async getVote(targetType, targetId, options) {
4340
+ return this._get(`/${targetType}/${targetId}/vote`, options);
4341
+ }
4342
+ // --------------------------------------------------------------------------
4343
+ // View counters
4344
+ // --------------------------------------------------------------------------
4345
+ /**
4346
+ * Record a view of a target. Increments the total counter
4347
+ * unconditionally; increments the unique-viewer counter once per
4348
+ * (viewer, UTC day).
4349
+ *
4350
+ * Authenticated callers don't need to pass `viewerFingerprint` — the
4351
+ * user_id is used. Anonymous callers may pass a 64-hex-char
4352
+ * fingerprint hash to participate in unique-viewer dedupe; without
4353
+ * one, only the total counter moves.
4354
+ */
4355
+ async recordView(targetType, targetId, viewerFingerprint, options) {
4356
+ const body = viewerFingerprint ? { viewer_fingerprint: viewerFingerprint } : {};
4357
+ return this.post(`/${targetType}/${targetId}/views`, body, options);
4358
+ }
4359
+ /**
4360
+ * Read view counters for a target.
4361
+ */
4362
+ async getViews(targetType, targetId, options) {
4363
+ return this._get(`/${targetType}/${targetId}/views`, options);
4364
+ }
4365
+ // --------------------------------------------------------------------------
4214
4366
  // Comments
4215
4367
  // --------------------------------------------------------------------------
4216
4368
  async comment(postId, data, options) {
@@ -5571,6 +5723,41 @@ var PhotoService = class extends ServiceModule {
5571
5723
  const qs = params.toString();
5572
5724
  return `${this.client.getBaseUrl()}${this.basePath}/${photoId}/transform${qs ? `?${qs}` : ""}`;
5573
5725
  }
5726
+ /**
5727
+ * Build an absolute URL for the **public** transform endpoint —
5728
+ * the unauthenticated path that serves transformed bytes for
5729
+ * `<img src>` use on logged-out marketing pages, blogs, embeds.
5730
+ *
5731
+ * The photo service refuses any photo that isn't:
5732
+ * - `visibility = 'anonymous_visible'` (the customer explicitly
5733
+ * opted into world-readable for this file at upload time)
5734
+ * - `scan_status = 'clean'` (no exception for pending/scanning)
5735
+ *
5736
+ * Anything else returns 404 — the endpoint refuses to disambiguate
5737
+ * "not found" from "not anonymous" so it can't be probed to
5738
+ * discover existence or visibility of private photos.
5739
+ *
5740
+ * Pair with {@link getTransformUrl} for the authenticated case:
5741
+ * customer apps that already have a session should keep using the
5742
+ * authed transform URL (which also handles `anonymous_visible`
5743
+ * photos transparently) — this URL is for cross-origin embedding.
5744
+ *
5745
+ * @example
5746
+ * ```tsx
5747
+ * // photo was uploaded with visibility: 'anonymous_visible'
5748
+ * <img src={sm.photo.getPublicTransformUrl(photoId, { width: 800 })} />
5749
+ * ```
5750
+ */
5751
+ getPublicTransformUrl(photoId, options) {
5752
+ const params = new URLSearchParams();
5753
+ if (options?.width) params.set("width", String(options.width));
5754
+ if (options?.height) params.set("height", String(options.height));
5755
+ if (options?.fit) params.set("fit", options.fit);
5756
+ if (options?.format) params.set("format", options.format);
5757
+ if (options?.quality) params.set("quality", String(options.quality));
5758
+ const qs = params.toString();
5759
+ return `${this.client.getBaseUrl()}${this.basePath}/public/${photoId}/transform${qs ? `?${qs}` : ""}`;
5760
+ }
5574
5761
  /**
5575
5762
  * Get a transform URL optimized for the given display area.
5576
5763
  *
@@ -5843,6 +6030,7 @@ var TtsService = class extends ServiceModule {
5843
6030
  "/synthesize",
5844
6031
  {
5845
6032
  text: params.text,
6033
+ speech_profile: params.speechProfile,
5846
6034
  voice: params.voice,
5847
6035
  model: params.model,
5848
6036
  provider: params.provider,
@@ -7976,6 +8164,7 @@ function ScaleMuleProvider({
7976
8164
  photo: baseClient.photo,
7977
8165
  video: baseClient.video,
7978
8166
  audio: baseClient.audio,
8167
+ media: baseClient.media,
7979
8168
  tts: baseClient.tts,
7980
8169
  mediaPolicy: effectiveMediaPolicy,
7981
8170
  user,
@@ -9537,148 +9726,43 @@ function useTtsJob(jobId, options) {
9537
9726
  return { job, loading, error, refresh };
9538
9727
  }
9539
9728
  function useMedia() {
9540
- const { storage, photo, video, audio, mediaPolicy: providerDefaultPolicy } = useScaleMule();
9729
+ const { media: rawMedia, mediaPolicy: providerDefaultPolicy } = useScaleMule();
9730
+ const media = rawMedia;
9541
9731
  const [uploading, setUploading] = React.useState(false);
9542
9732
  const [error, setError] = React.useState(null);
9543
9733
  const upload = React.useCallback(
9544
9734
  async (file, options) => {
9545
9735
  setUploading(true);
9546
9736
  setError(null);
9547
- const visibility = options?.visibility ?? (options?.is_public === true ? "app_public" : options?.is_public === false ? "private" : "private");
9548
- const isPublic = visibility !== "private";
9549
- const isAnonymous = visibility === "anonymous_visible";
9550
- const mimeType = file.type || "application/octet-stream";
9551
9737
  const policy = options?.policy ?? providerDefaultPolicy ?? "safe_visible";
9552
- const gateOnPipeline = policy === "safe_public" || policy === "moderated" || policy === "compliance";
9553
- const sharedOpts = {
9554
- filename: options?.filename,
9555
- metadata: options?.metadata,
9556
- onProgress: options?.onProgress,
9557
- signal: options?.signal
9558
- };
9559
- const storageAnon = storage;
9560
9738
  try {
9561
- if (isAnonymous) {
9562
- const r2 = await storageAnon.uploadAnonymous(file, sharedOpts);
9563
- if (r2.error || !r2.data) {
9564
- throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9565
- }
9566
- const f2 = r2.data;
9567
- return {
9568
- file_id: f2.id,
9569
- photo_id: null,
9570
- // For anon files, original_view_url is the same unsigned
9571
- // CDN URL — no signed alternative exists. Callers should
9572
- // prefer `cdn_url` (more specific) but `original_view_url`
9573
- // works too for back-compat with code that already reads
9574
- // that field.
9575
- original_view_url: f2.cdn_url ?? null,
9576
- optimized_url_promise: Promise.resolve(null),
9577
- hls_url_promise: Promise.resolve(null),
9578
- mime_type: mimeType,
9579
- is_public: true,
9580
- visibility: "anonymous_visible",
9581
- cdn_url: f2.cdn_url ?? null
9582
- };
9583
- }
9584
- if (mimeType.startsWith("image/") && !options?.skipPhotoRegister && !isPublic) {
9585
- const r2 = await photo.uploadViaStorage(file, sharedOpts);
9586
- if (r2.error || !r2.data) {
9587
- throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9588
- }
9589
- if (gateOnPipeline) {
9590
- await r2.data.optimized_url_promise;
9591
- }
9592
- return {
9593
- file_id: r2.data.file_id,
9594
- photo_id: r2.data.photo_id,
9595
- original_view_url: r2.data.original_view_url,
9596
- optimized_url_promise: r2.data.optimized_url_promise,
9597
- hls_url_promise: Promise.resolve(null),
9598
- mime_type: mimeType,
9599
- is_public: false,
9600
- visibility: "private",
9601
- cdn_url: null
9602
- };
9603
- }
9604
- if (mimeType.startsWith("video/") && !isPublic) {
9605
- const r2 = await video.uploadViaStorage(file, sharedOpts);
9606
- if (r2.error || !r2.data) {
9607
- throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9608
- }
9609
- if (gateOnPipeline) {
9610
- await r2.data.hls_url_promise;
9611
- }
9612
- return {
9613
- file_id: r2.data.file_id,
9614
- photo_id: null,
9615
- original_view_url: r2.data.original_view_url,
9616
- optimized_url_promise: Promise.resolve(null),
9617
- hls_url_promise: r2.data.hls_url_promise,
9618
- mime_type: mimeType,
9619
- is_public: false,
9620
- visibility: "private",
9621
- cdn_url: null
9622
- };
9623
- }
9624
- if (mimeType.startsWith("audio/") && !isPublic) {
9625
- const r2 = await audio.uploadViaStorage(file, sharedOpts);
9626
- if (r2.error || !r2.data) {
9627
- throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9628
- }
9629
- return {
9630
- file_id: r2.data.file_id,
9631
- photo_id: null,
9632
- original_view_url: r2.data.original_view_url,
9633
- optimized_url_promise: Promise.resolve(null),
9634
- hls_url_promise: Promise.resolve(null),
9635
- mime_type: mimeType,
9636
- is_public: false,
9637
- visibility: "private",
9638
- cdn_url: null
9639
- };
9640
- }
9641
- if (mimeType.startsWith("image/") && isPublic) {
9642
- const r2 = await storage.upload(file, {
9643
- ...sharedOpts,
9644
- visibility: "app_public",
9645
- skipCompression: true
9646
- });
9647
- if (r2.error || !r2.data) {
9648
- throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9739
+ const result = await media.upload(file, {
9740
+ visibility: options?.visibility,
9741
+ isPublic: options?.is_public,
9742
+ policy,
9743
+ filename: options?.filename,
9744
+ metadata: options?.metadata,
9745
+ signal: options?.signal,
9746
+ skipPhotoRegister: options?.skipPhotoRegister,
9747
+ onProgress: (event) => {
9748
+ if (typeof event.progress === "number") {
9749
+ options?.onProgress?.(event.progress);
9750
+ }
9649
9751
  }
9650
- const f2 = r2.data;
9651
- return {
9652
- file_id: f2.id,
9653
- photo_id: null,
9654
- original_view_url: f2.url ?? null,
9655
- optimized_url_promise: Promise.resolve(null),
9656
- hls_url_promise: Promise.resolve(null),
9657
- mime_type: mimeType,
9658
- is_public: f2.is_public ?? true,
9659
- visibility: f2.visibility ?? "app_public",
9660
- cdn_url: f2.cdn_url ?? null
9661
- };
9662
- }
9663
- const r = isPublic ? await storage.upload(file, {
9664
- ...sharedOpts,
9665
- visibility: "app_public",
9666
- skipCompression: true
9667
- }) : await storage.uploadPrivate(file, sharedOpts);
9668
- if (r.error || !r.data) {
9669
- throw r.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9752
+ });
9753
+ if (result.error || !result.data) {
9754
+ throw result.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9670
9755
  }
9671
- const f = r.data;
9672
9756
  return {
9673
- file_id: f.id,
9674
- photo_id: null,
9675
- original_view_url: f.url ?? null,
9676
- optimized_url_promise: Promise.resolve(null),
9677
- hls_url_promise: Promise.resolve(null),
9678
- mime_type: mimeType,
9679
- is_public: f.is_public ?? isPublic,
9680
- visibility: f.visibility ?? (isPublic ? "app_public" : "private"),
9681
- cdn_url: f.cdn_url ?? null
9757
+ file_id: result.data.file_id,
9758
+ photo_id: result.data.photo_id,
9759
+ original_view_url: result.data.original_view_url,
9760
+ optimized_url_promise: result.data.optimized_url_promise,
9761
+ hls_url_promise: result.data.hls_url_promise,
9762
+ mime_type: result.data.mime_type,
9763
+ is_public: result.data.is_public,
9764
+ visibility: result.data.visibility,
9765
+ cdn_url: result.data.cdn_url
9682
9766
  };
9683
9767
  } catch (err) {
9684
9768
  const e = err;
@@ -9688,19 +9772,16 @@ function useMedia() {
9688
9772
  setUploading(false);
9689
9773
  }
9690
9774
  },
9691
- [storage, photo, video, audio, providerDefaultPolicy]
9775
+ [media, providerDefaultPolicy]
9692
9776
  );
9693
9777
  const cancelUpload = React.useCallback(
9694
9778
  async (fileId) => {
9695
9779
  setError(null);
9696
9780
  try {
9697
- const [, storageResult] = await Promise.all([
9698
- photo.delete(fileId).catch(() => void 0),
9699
- storage.delete(fileId)
9700
- ]);
9701
- if (storageResult.error) {
9702
- if (storageResult.error.status === 404) return;
9703
- throw storageResult.error;
9781
+ const result = await media.delete(fileId);
9782
+ if (result.error) {
9783
+ if (result.error.status === 404) return;
9784
+ throw result.error;
9704
9785
  }
9705
9786
  } catch (err) {
9706
9787
  const e = err;
@@ -9708,7 +9789,7 @@ function useMedia() {
9708
9789
  throw e;
9709
9790
  }
9710
9791
  },
9711
- [storage, photo]
9792
+ [media]
9712
9793
  );
9713
9794
  return { upload, cancelUpload, error, uploading };
9714
9795
  }
package/dist/index.mjs CHANGED
@@ -2093,6 +2093,24 @@ var DEFAULT_STALL_TIMEOUT_MS = 45e3;
2093
2093
  var SLOW_NETWORK_STALL_TIMEOUT_MS = 9e4;
2094
2094
  var MULTIPART_THRESHOLD = 8 * 1024 * 1024;
2095
2095
  var MULTIPART_THRESHOLD_SLOW = 4 * 1024 * 1024;
2096
+ function buildVisibilityWire(options) {
2097
+ if (!options) return {};
2098
+ if (options.visibility) {
2099
+ return {
2100
+ visibility: options.visibility,
2101
+ // Legacy field for old storage instances that haven't deployed
2102
+ // the tristate handler yet — omit `is_public` for the
2103
+ // `anonymous_visible` case so an old server that doesn't know
2104
+ // the new value at least falls through to its own default
2105
+ // rather than mis-treating it as `app_public`.
2106
+ ...options.visibility === "private" ? { is_public: false } : options.visibility === "app_public" ? { is_public: true } : {}
2107
+ };
2108
+ }
2109
+ if (typeof options.isPublic === "boolean") {
2110
+ return { is_public: options.isPublic };
2111
+ }
2112
+ return {};
2113
+ }
2096
2114
  var StorageService = class extends ServiceModule {
2097
2115
  constructor() {
2098
2116
  super(...arguments);
@@ -2174,6 +2192,42 @@ var StorageService = class extends ServiceModule {
2174
2192
  }
2175
2193
  return result;
2176
2194
  }
2195
+ /**
2196
+ * Upload a file as world-readable on the anonymous public CDN.
2197
+ *
2198
+ * Same browser→S3 pipeline as {@link upload}, but pins
2199
+ * `visibility: 'anonymous_visible'` — caller cannot opt out.
2200
+ *
2201
+ * Use this for media meant to render on logged-out marketing
2202
+ * pages, blog posts, embed snippets — anywhere a customer needs
2203
+ * to drop a URL into `<img src>` without an authenticated
2204
+ * session. For everything else (chat attachments, private
2205
+ * uploads, app-internal galleries) prefer {@link upload} or
2206
+ * {@link uploadPrivate}.
2207
+ *
2208
+ * **`cdn_url` may be `null` on the immediate response.** Storage
2209
+ * intentionally withholds the public CDN URL until the AV scan
2210
+ * has flipped to `clean` — exposing the URL pre-scan would let a
2211
+ * customer embed the bytes in a logged-out page before the
2212
+ * platform has verified them. The upload itself succeeds
2213
+ * regardless; consumers should check `result.data.cdn_url` and:
2214
+ * - if non-null → it's safe to publish
2215
+ * - if null → poll {@link getFileStatus} on a small backoff
2216
+ * until `urls.cdn_url` populates (or `scan.status` flips to
2217
+ * `threat` / `quarantined` / `error` — terminal failure)
2218
+ *
2219
+ * Requires the operator to have provisioned the anonymous
2220
+ * delivery bucket + CDN. When they haven't, the storage service
2221
+ * returns 503 `ANONYMOUS_DELIVERY_NOT_CONFIGURED` (an error
2222
+ * propagated through this helper) — it never silently demotes
2223
+ * to `app_public`.
2224
+ */
2225
+ async uploadAnonymous(file, options) {
2226
+ return this.upload(file, {
2227
+ ...options,
2228
+ visibility: "anonymous_visible"
2229
+ });
2230
+ }
2177
2231
  // --------------------------------------------------------------------------
2178
2232
  // Direct Upload (3-step with retry + stall)
2179
2233
  // --------------------------------------------------------------------------
@@ -2187,7 +2241,7 @@ var StorageService = class extends ServiceModule {
2187
2241
  filename,
2188
2242
  content_type: file.type || "application/octet-stream",
2189
2243
  size_bytes: file.size,
2190
- is_public: options?.isPublic ?? true,
2244
+ ...buildVisibilityWire(options),
2191
2245
  metadata: options?.metadata
2192
2246
  },
2193
2247
  requestOpts
@@ -2278,6 +2332,13 @@ var StorageService = class extends ServiceModule {
2278
2332
  content_type: d.content_type,
2279
2333
  size_bytes: d.size_bytes,
2280
2334
  url: d.url,
2335
+ // Surface visibility + cdn_url all the way out so callers
2336
+ // can branch on visibility without a follow-up getInfo()
2337
+ // round-trip. Anonymous-visible callers especially need
2338
+ // `cdn_url` here to render the image immediately.
2339
+ visibility: d.visibility,
2340
+ cdn_url: d.cdn_url,
2341
+ is_public: d.visibility ? d.visibility !== "private" : void 0,
2281
2342
  created_at: (/* @__PURE__ */ new Date()).toISOString()
2282
2343
  },
2283
2344
  error: null
@@ -2359,7 +2420,7 @@ var StorageService = class extends ServiceModule {
2359
2420
  filename,
2360
2421
  content_type: file.type || "application/octet-stream",
2361
2422
  size_bytes: file.size,
2362
- is_public: options?.isPublic ?? true,
2423
+ ...buildVisibilityWire(options),
2363
2424
  metadata: options?.metadata,
2364
2425
  chunk_size: options?.chunkSize,
2365
2426
  ...clientUploadKey ? { client_upload_key: clientUploadKey } : {}
@@ -2637,6 +2698,9 @@ var StorageService = class extends ServiceModule {
2637
2698
  content_type: d.content_type,
2638
2699
  size_bytes: d.size_bytes,
2639
2700
  url: d.url,
2701
+ visibility: d.visibility,
2702
+ cdn_url: d.cdn_url,
2703
+ is_public: d.visibility ? d.visibility !== "private" : void 0,
2640
2704
  created_at: (/* @__PURE__ */ new Date()).toISOString()
2641
2705
  },
2642
2706
  error: null
@@ -2674,7 +2738,7 @@ var StorageService = class extends ServiceModule {
2674
2738
  {
2675
2739
  filename,
2676
2740
  content_type: contentType,
2677
- is_public: options?.isPublic ?? true,
2741
+ ...buildVisibilityWire(options),
2678
2742
  expires_in: options?.expiresIn ?? 3600,
2679
2743
  size_bytes: options?.sizeBytes,
2680
2744
  metadata: options?.metadata
@@ -2721,7 +2785,16 @@ var StorageService = class extends ServiceModule {
2721
2785
  // --------------------------------------------------------------------------
2722
2786
  // Multipart Public API (for advanced/server-side usage)
2723
2787
  // --------------------------------------------------------------------------
2724
- /** Start a multipart upload session. */
2788
+ /**
2789
+ * Start a multipart upload session.
2790
+ *
2791
+ * Accepts both the typed `visibility` field (preferred) and the
2792
+ * legacy `is_public` boolean for back-compat. If both are sent the
2793
+ * storage service uses `visibility`. `'anonymous_visible'` requires
2794
+ * the operator to have provisioned the anonymous bucket — the
2795
+ * service returns 503 `ANONYMOUS_DELIVERY_NOT_CONFIGURED`
2796
+ * otherwise (the SDK never silently demotes).
2797
+ */
2725
2798
  async startMultipartUpload(params, requestOptions) {
2726
2799
  return this.post("/signed-url/multipart/start", params, requestOptions);
2727
2800
  }
@@ -2852,14 +2925,26 @@ var StorageService = class extends ServiceModule {
2852
2925
  return this._get(`/files/${fileId}/view-status`, options);
2853
2926
  }
2854
2927
  /**
2855
- * Update a file's visibility (public/private).
2856
- * Only the file owner can toggle this. Changes URL TTL — does not move the S3 object.
2857
- * Public files get 7-day signed URLs; private files get 1-hour signed URLs.
2928
+ * Update a file's visibility.
2929
+ *
2930
+ * Accepts either:
2931
+ * - a tri-state {@link Visibility} string (preferred), or
2932
+ * - a legacy boolean (`true` → `app_public`, `false` → `private`).
2933
+ *
2934
+ * Only the file owner / app member can toggle this. Same-bucket
2935
+ * transitions (`private` ↔ `app_public`) succeed and the service
2936
+ * keeps the legacy `is_public` column in sync via a DB trigger.
2937
+ * Cross-bucket transitions (any flip into or out of
2938
+ * `'anonymous_visible'`) currently return 409
2939
+ * `VISIBILITY_TRANSITION_UNSUPPORTED` — the bytes would need to
2940
+ * move between S3 buckets and that orchestration is not yet
2941
+ * shipped. Re-upload the file with the desired visibility instead.
2858
2942
  */
2859
- async updateVisibility(fileId, isPublic, options) {
2943
+ async updateVisibility(fileId, visibility, options) {
2944
+ const body = typeof visibility === "boolean" ? { is_public: visibility } : { visibility };
2860
2945
  return this.patch(
2861
2946
  `/files/${fileId}/visibility`,
2862
- { is_public: isPublic },
2947
+ body,
2863
2948
  options
2864
2949
  );
2865
2950
  }
@@ -2957,6 +3042,7 @@ var StorageService = class extends ServiceModule {
2957
3042
  clearTimeout(timer);
2958
3043
  parentSignalCleanup?.();
2959
3044
  if (!response.ok) {
3045
+ const s3ErrorBody = await response.text().catch(() => "");
2960
3046
  return {
2961
3047
  data: null,
2962
3048
  error: {
@@ -2966,7 +3052,9 @@ var StorageService = class extends ServiceModule {
2966
3052
  details: {
2967
3053
  transport: "fetch",
2968
3054
  total_bytes: file.size,
2969
- online: getOnlineStatus()
3055
+ online: getOnlineStatus(),
3056
+ s3_error_body: s3ErrorBody.slice(0, 1024),
3057
+ s3_error_code: extractS3ErrorCode(s3ErrorBody)
2970
3058
  }
2971
3059
  }
2972
3060
  };
@@ -3057,6 +3145,7 @@ var StorageService = class extends ServiceModule {
3057
3145
  onProgress?.(100);
3058
3146
  resolve({ data: null, error: null });
3059
3147
  } else {
3148
+ const body = (xhr.responseText || "").slice(0, 1024);
3060
3149
  resolve({
3061
3150
  data: null,
3062
3151
  error: {
@@ -3068,7 +3157,9 @@ var StorageService = class extends ServiceModule {
3068
3157
  bytes_sent: lastLoaded,
3069
3158
  total_bytes: totalBytes,
3070
3159
  progress_percent: totalBytes > 0 ? Math.round(lastLoaded / totalBytes * 100) : void 0,
3071
- online: getOnlineStatus()
3160
+ online: getOnlineStatus(),
3161
+ s3_error_body: body,
3162
+ s3_error_code: extractS3ErrorCode(body)
3072
3163
  }
3073
3164
  }
3074
3165
  });
@@ -3147,12 +3238,18 @@ var StorageService = class extends ServiceModule {
3147
3238
  }
3148
3239
  return { etag };
3149
3240
  }
3241
+ const body = await response.text().catch(() => "");
3242
+ const s3ErrorCode = extractS3ErrorCode(body);
3150
3243
  if (NON_RETRYABLE_STATUS_CODES.has(response.status)) {
3151
- return { error: `Part upload failed: ${response.status}`, status: response.status, code: "s3_error" };
3244
+ return {
3245
+ error: `Part upload failed: ${response.status}${s3ErrorCode ? ` (${s3ErrorCode})` : ""}`,
3246
+ status: response.status,
3247
+ code: response.status === 403 ? "s3_signature_error" : "s3_error"
3248
+ };
3152
3249
  }
3153
3250
  if (attempt === RETRY_DELAYS.length - 1) {
3154
3251
  return {
3155
- error: `Part upload failed after retries: ${response.status}`,
3252
+ error: `Part upload failed after retries: ${response.status}${s3ErrorCode ? ` (${s3ErrorCode})` : ""}`,
3156
3253
  status: response.status,
3157
3254
  code: "s3_error"
3158
3255
  };
@@ -3278,6 +3375,11 @@ function getUploadEnvironmentDiagnostics() {
3278
3375
  function asNumber2(value) {
3279
3376
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3280
3377
  }
3378
+ function extractS3ErrorCode(body) {
3379
+ if (!body) return void 0;
3380
+ const m = body.match(/<Code>([^<]+)<\/Code>/);
3381
+ return m ? m[1] : void 0;
3382
+ }
3281
3383
  var DEFAULT_RECONNECT_BASE_MS = 1e3;
3282
3384
  var MAX_RECONNECT_MS = 3e4;
3283
3385
  var HEARTBEAT_INTERVAL_MS = 3e4;
@@ -4191,6 +4293,56 @@ var SocialService = class extends ServiceModule {
4191
4293
  return this._list(`/${targetType}/${targetId}/likes`, params, requestOptions);
4192
4294
  }
4193
4295
  // --------------------------------------------------------------------------
4296
+ // Voting (bipolar — up/down with score)
4297
+ // --------------------------------------------------------------------------
4298
+ /**
4299
+ * Cast, change, or clear the caller's vote on a target.
4300
+ *
4301
+ * `value`: 1 = upvote, -1 = downvote, 0 = clear.
4302
+ *
4303
+ * Idempotent: re-casting the same value is a no-op; clearing when no
4304
+ * vote exists is a no-op. Server validates `target_type` matches
4305
+ * `[a-z0-9_]{1,64}`. Convention: prefix per app (`weekmob_post`,
4306
+ * `gistyo_gist`).
4307
+ *
4308
+ * Requires an authenticated session; anonymous calls return 401.
4309
+ */
4310
+ async vote(targetType, targetId, value, options) {
4311
+ return this.put(`/${targetType}/${targetId}/vote`, { value }, options);
4312
+ }
4313
+ /**
4314
+ * Read the caller's current vote on a target plus the aggregate counts.
4315
+ * `value` is 0 when the caller has no vote.
4316
+ *
4317
+ * Requires an authenticated session.
4318
+ */
4319
+ async getVote(targetType, targetId, options) {
4320
+ return this._get(`/${targetType}/${targetId}/vote`, options);
4321
+ }
4322
+ // --------------------------------------------------------------------------
4323
+ // View counters
4324
+ // --------------------------------------------------------------------------
4325
+ /**
4326
+ * Record a view of a target. Increments the total counter
4327
+ * unconditionally; increments the unique-viewer counter once per
4328
+ * (viewer, UTC day).
4329
+ *
4330
+ * Authenticated callers don't need to pass `viewerFingerprint` — the
4331
+ * user_id is used. Anonymous callers may pass a 64-hex-char
4332
+ * fingerprint hash to participate in unique-viewer dedupe; without
4333
+ * one, only the total counter moves.
4334
+ */
4335
+ async recordView(targetType, targetId, viewerFingerprint, options) {
4336
+ const body = viewerFingerprint ? { viewer_fingerprint: viewerFingerprint } : {};
4337
+ return this.post(`/${targetType}/${targetId}/views`, body, options);
4338
+ }
4339
+ /**
4340
+ * Read view counters for a target.
4341
+ */
4342
+ async getViews(targetType, targetId, options) {
4343
+ return this._get(`/${targetType}/${targetId}/views`, options);
4344
+ }
4345
+ // --------------------------------------------------------------------------
4194
4346
  // Comments
4195
4347
  // --------------------------------------------------------------------------
4196
4348
  async comment(postId, data, options) {
@@ -5551,6 +5703,41 @@ var PhotoService = class extends ServiceModule {
5551
5703
  const qs = params.toString();
5552
5704
  return `${this.client.getBaseUrl()}${this.basePath}/${photoId}/transform${qs ? `?${qs}` : ""}`;
5553
5705
  }
5706
+ /**
5707
+ * Build an absolute URL for the **public** transform endpoint —
5708
+ * the unauthenticated path that serves transformed bytes for
5709
+ * `<img src>` use on logged-out marketing pages, blogs, embeds.
5710
+ *
5711
+ * The photo service refuses any photo that isn't:
5712
+ * - `visibility = 'anonymous_visible'` (the customer explicitly
5713
+ * opted into world-readable for this file at upload time)
5714
+ * - `scan_status = 'clean'` (no exception for pending/scanning)
5715
+ *
5716
+ * Anything else returns 404 — the endpoint refuses to disambiguate
5717
+ * "not found" from "not anonymous" so it can't be probed to
5718
+ * discover existence or visibility of private photos.
5719
+ *
5720
+ * Pair with {@link getTransformUrl} for the authenticated case:
5721
+ * customer apps that already have a session should keep using the
5722
+ * authed transform URL (which also handles `anonymous_visible`
5723
+ * photos transparently) — this URL is for cross-origin embedding.
5724
+ *
5725
+ * @example
5726
+ * ```tsx
5727
+ * // photo was uploaded with visibility: 'anonymous_visible'
5728
+ * <img src={sm.photo.getPublicTransformUrl(photoId, { width: 800 })} />
5729
+ * ```
5730
+ */
5731
+ getPublicTransformUrl(photoId, options) {
5732
+ const params = new URLSearchParams();
5733
+ if (options?.width) params.set("width", String(options.width));
5734
+ if (options?.height) params.set("height", String(options.height));
5735
+ if (options?.fit) params.set("fit", options.fit);
5736
+ if (options?.format) params.set("format", options.format);
5737
+ if (options?.quality) params.set("quality", String(options.quality));
5738
+ const qs = params.toString();
5739
+ return `${this.client.getBaseUrl()}${this.basePath}/public/${photoId}/transform${qs ? `?${qs}` : ""}`;
5740
+ }
5554
5741
  /**
5555
5742
  * Get a transform URL optimized for the given display area.
5556
5743
  *
@@ -5823,6 +6010,7 @@ var TtsService = class extends ServiceModule {
5823
6010
  "/synthesize",
5824
6011
  {
5825
6012
  text: params.text,
6013
+ speech_profile: params.speechProfile,
5826
6014
  voice: params.voice,
5827
6015
  model: params.model,
5828
6016
  provider: params.provider,
@@ -7956,6 +8144,7 @@ function ScaleMuleProvider({
7956
8144
  photo: baseClient.photo,
7957
8145
  video: baseClient.video,
7958
8146
  audio: baseClient.audio,
8147
+ media: baseClient.media,
7959
8148
  tts: baseClient.tts,
7960
8149
  mediaPolicy: effectiveMediaPolicy,
7961
8150
  user,
@@ -9517,148 +9706,43 @@ function useTtsJob(jobId, options) {
9517
9706
  return { job, loading, error, refresh };
9518
9707
  }
9519
9708
  function useMedia() {
9520
- const { storage, photo, video, audio, mediaPolicy: providerDefaultPolicy } = useScaleMule();
9709
+ const { media: rawMedia, mediaPolicy: providerDefaultPolicy } = useScaleMule();
9710
+ const media = rawMedia;
9521
9711
  const [uploading, setUploading] = useState(false);
9522
9712
  const [error, setError] = useState(null);
9523
9713
  const upload = useCallback(
9524
9714
  async (file, options) => {
9525
9715
  setUploading(true);
9526
9716
  setError(null);
9527
- const visibility = options?.visibility ?? (options?.is_public === true ? "app_public" : options?.is_public === false ? "private" : "private");
9528
- const isPublic = visibility !== "private";
9529
- const isAnonymous = visibility === "anonymous_visible";
9530
- const mimeType = file.type || "application/octet-stream";
9531
9717
  const policy = options?.policy ?? providerDefaultPolicy ?? "safe_visible";
9532
- const gateOnPipeline = policy === "safe_public" || policy === "moderated" || policy === "compliance";
9533
- const sharedOpts = {
9534
- filename: options?.filename,
9535
- metadata: options?.metadata,
9536
- onProgress: options?.onProgress,
9537
- signal: options?.signal
9538
- };
9539
- const storageAnon = storage;
9540
9718
  try {
9541
- if (isAnonymous) {
9542
- const r2 = await storageAnon.uploadAnonymous(file, sharedOpts);
9543
- if (r2.error || !r2.data) {
9544
- throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9545
- }
9546
- const f2 = r2.data;
9547
- return {
9548
- file_id: f2.id,
9549
- photo_id: null,
9550
- // For anon files, original_view_url is the same unsigned
9551
- // CDN URL — no signed alternative exists. Callers should
9552
- // prefer `cdn_url` (more specific) but `original_view_url`
9553
- // works too for back-compat with code that already reads
9554
- // that field.
9555
- original_view_url: f2.cdn_url ?? null,
9556
- optimized_url_promise: Promise.resolve(null),
9557
- hls_url_promise: Promise.resolve(null),
9558
- mime_type: mimeType,
9559
- is_public: true,
9560
- visibility: "anonymous_visible",
9561
- cdn_url: f2.cdn_url ?? null
9562
- };
9563
- }
9564
- if (mimeType.startsWith("image/") && !options?.skipPhotoRegister && !isPublic) {
9565
- const r2 = await photo.uploadViaStorage(file, sharedOpts);
9566
- if (r2.error || !r2.data) {
9567
- throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9568
- }
9569
- if (gateOnPipeline) {
9570
- await r2.data.optimized_url_promise;
9571
- }
9572
- return {
9573
- file_id: r2.data.file_id,
9574
- photo_id: r2.data.photo_id,
9575
- original_view_url: r2.data.original_view_url,
9576
- optimized_url_promise: r2.data.optimized_url_promise,
9577
- hls_url_promise: Promise.resolve(null),
9578
- mime_type: mimeType,
9579
- is_public: false,
9580
- visibility: "private",
9581
- cdn_url: null
9582
- };
9583
- }
9584
- if (mimeType.startsWith("video/") && !isPublic) {
9585
- const r2 = await video.uploadViaStorage(file, sharedOpts);
9586
- if (r2.error || !r2.data) {
9587
- throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9588
- }
9589
- if (gateOnPipeline) {
9590
- await r2.data.hls_url_promise;
9591
- }
9592
- return {
9593
- file_id: r2.data.file_id,
9594
- photo_id: null,
9595
- original_view_url: r2.data.original_view_url,
9596
- optimized_url_promise: Promise.resolve(null),
9597
- hls_url_promise: r2.data.hls_url_promise,
9598
- mime_type: mimeType,
9599
- is_public: false,
9600
- visibility: "private",
9601
- cdn_url: null
9602
- };
9603
- }
9604
- if (mimeType.startsWith("audio/") && !isPublic) {
9605
- const r2 = await audio.uploadViaStorage(file, sharedOpts);
9606
- if (r2.error || !r2.data) {
9607
- throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9608
- }
9609
- return {
9610
- file_id: r2.data.file_id,
9611
- photo_id: null,
9612
- original_view_url: r2.data.original_view_url,
9613
- optimized_url_promise: Promise.resolve(null),
9614
- hls_url_promise: Promise.resolve(null),
9615
- mime_type: mimeType,
9616
- is_public: false,
9617
- visibility: "private",
9618
- cdn_url: null
9619
- };
9620
- }
9621
- if (mimeType.startsWith("image/") && isPublic) {
9622
- const r2 = await storage.upload(file, {
9623
- ...sharedOpts,
9624
- visibility: "app_public",
9625
- skipCompression: true
9626
- });
9627
- if (r2.error || !r2.data) {
9628
- throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9719
+ const result = await media.upload(file, {
9720
+ visibility: options?.visibility,
9721
+ isPublic: options?.is_public,
9722
+ policy,
9723
+ filename: options?.filename,
9724
+ metadata: options?.metadata,
9725
+ signal: options?.signal,
9726
+ skipPhotoRegister: options?.skipPhotoRegister,
9727
+ onProgress: (event) => {
9728
+ if (typeof event.progress === "number") {
9729
+ options?.onProgress?.(event.progress);
9730
+ }
9629
9731
  }
9630
- const f2 = r2.data;
9631
- return {
9632
- file_id: f2.id,
9633
- photo_id: null,
9634
- original_view_url: f2.url ?? null,
9635
- optimized_url_promise: Promise.resolve(null),
9636
- hls_url_promise: Promise.resolve(null),
9637
- mime_type: mimeType,
9638
- is_public: f2.is_public ?? true,
9639
- visibility: f2.visibility ?? "app_public",
9640
- cdn_url: f2.cdn_url ?? null
9641
- };
9642
- }
9643
- const r = isPublic ? await storage.upload(file, {
9644
- ...sharedOpts,
9645
- visibility: "app_public",
9646
- skipCompression: true
9647
- }) : await storage.uploadPrivate(file, sharedOpts);
9648
- if (r.error || !r.data) {
9649
- throw r.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9732
+ });
9733
+ if (result.error || !result.data) {
9734
+ throw result.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
9650
9735
  }
9651
- const f = r.data;
9652
9736
  return {
9653
- file_id: f.id,
9654
- photo_id: null,
9655
- original_view_url: f.url ?? null,
9656
- optimized_url_promise: Promise.resolve(null),
9657
- hls_url_promise: Promise.resolve(null),
9658
- mime_type: mimeType,
9659
- is_public: f.is_public ?? isPublic,
9660
- visibility: f.visibility ?? (isPublic ? "app_public" : "private"),
9661
- cdn_url: f.cdn_url ?? null
9737
+ file_id: result.data.file_id,
9738
+ photo_id: result.data.photo_id,
9739
+ original_view_url: result.data.original_view_url,
9740
+ optimized_url_promise: result.data.optimized_url_promise,
9741
+ hls_url_promise: result.data.hls_url_promise,
9742
+ mime_type: result.data.mime_type,
9743
+ is_public: result.data.is_public,
9744
+ visibility: result.data.visibility,
9745
+ cdn_url: result.data.cdn_url
9662
9746
  };
9663
9747
  } catch (err) {
9664
9748
  const e = err;
@@ -9668,19 +9752,16 @@ function useMedia() {
9668
9752
  setUploading(false);
9669
9753
  }
9670
9754
  },
9671
- [storage, photo, video, audio, providerDefaultPolicy]
9755
+ [media, providerDefaultPolicy]
9672
9756
  );
9673
9757
  const cancelUpload = useCallback(
9674
9758
  async (fileId) => {
9675
9759
  setError(null);
9676
9760
  try {
9677
- const [, storageResult] = await Promise.all([
9678
- photo.delete(fileId).catch(() => void 0),
9679
- storage.delete(fileId)
9680
- ]);
9681
- if (storageResult.error) {
9682
- if (storageResult.error.status === 404) return;
9683
- throw storageResult.error;
9761
+ const result = await media.delete(fileId);
9762
+ if (result.error) {
9763
+ if (result.error.status === 404) return;
9764
+ throw result.error;
9684
9765
  }
9685
9766
  } catch (err) {
9686
9767
  const e = err;
@@ -9688,7 +9769,7 @@ function useMedia() {
9688
9769
  throw e;
9689
9770
  }
9690
9771
  },
9691
- [storage, photo]
9772
+ [media]
9692
9773
  );
9693
9774
  return { upload, cancelUpload, error, uploading };
9694
9775
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/nextjs",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
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",
@@ -60,7 +60,7 @@
60
60
  "dependencies": {
61
61
  "@scalemule/ledvery": "^0.0.1",
62
62
  "@scalemule/money": "^0.0.1",
63
- "@scalemule/sdk": "^0.0.55"
63
+ "@scalemule/sdk": "^0.0.56"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "next": ">=14.0.0",