@scalemule/nextjs 0.1.9 → 0.1.11

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
@@ -216,13 +216,13 @@ interface ScaleMuleProviderProps extends ScaleMuleConfig {
216
216
  *
217
217
  * The platform stores the per-app policy in
218
218
  * `application_storage_settings.media_policy` (Phase 4 / P3, live in
219
- * prod). That endpoint is `MemberOnly` admin-auth, so end-user-context
220
- * provider can't fetch it directly — apps that want policy-driven
221
- * defaults should declare the value here, mirroring whatever the
222
- * platform admin set.
219
+ * prod). On boot, the provider fetches `GET /v1/storage/policy` (a
220
+ * lightweight `EndUserOnly` endpoint added in `@scalemule/sdk@0.0.45`)
221
+ * and uses the returned policy as the effective default.
223
222
  *
224
- * Default: undefined → `useMedia()` falls back to its built-in
225
- * `safe_visible` default.
223
+ * Passing this prop overrides the auto-fetched value — useful for
224
+ * tests or when an app needs to force a specific mode regardless of
225
+ * platform config.
226
226
  */
227
227
  mediaPolicy?: MediaPolicy;
228
228
  }
package/dist/index.d.ts CHANGED
@@ -216,13 +216,13 @@ interface ScaleMuleProviderProps extends ScaleMuleConfig {
216
216
  *
217
217
  * The platform stores the per-app policy in
218
218
  * `application_storage_settings.media_policy` (Phase 4 / P3, live in
219
- * prod). That endpoint is `MemberOnly` admin-auth, so end-user-context
220
- * provider can't fetch it directly — apps that want policy-driven
221
- * defaults should declare the value here, mirroring whatever the
222
- * platform admin set.
219
+ * prod). On boot, the provider fetches `GET /v1/storage/policy` (a
220
+ * lightweight `EndUserOnly` endpoint added in `@scalemule/sdk@0.0.45`)
221
+ * and uses the returned policy as the effective default.
223
222
  *
224
- * Default: undefined → `useMedia()` falls back to its built-in
225
- * `safe_visible` default.
223
+ * Passing this prop overrides the auto-fetched value — useful for
224
+ * tests or when an app needs to force a specific mode regardless of
225
+ * platform config.
226
226
  */
227
227
  mediaPolicy?: MediaPolicy;
228
228
  }
package/dist/index.js CHANGED
@@ -2771,6 +2771,20 @@ var StorageService = class extends ServiceModule {
2771
2771
  async getFileStatus(fileId, options) {
2772
2772
  return this._get(`/files/${fileId}/status`, options);
2773
2773
  }
2774
+ /**
2775
+ * Read the application's active media policy. Lightweight endpoint
2776
+ * (`GET /v1/storage/policy`) used by the SDK on boot to pick up the
2777
+ * platform-default `media_policy` without requiring app-admin auth.
2778
+ *
2779
+ * @example
2780
+ * ```ts
2781
+ * const { data } = await client.storage.getPolicy();
2782
+ * console.log(data?.media_policy); // 'safe_visible'
2783
+ * ```
2784
+ */
2785
+ async getPolicy(options) {
2786
+ return this._get(`/policy`, options);
2787
+ }
2774
2788
  /**
2775
2789
  * Get a signed view URL for inline display (img src, thumbnails).
2776
2790
  * Returns CloudFront signed URL (fast, ~1us) or S3 presigned fallback.
@@ -7569,6 +7583,28 @@ function ScaleMuleProvider({
7569
7583
  debug
7570
7584
  });
7571
7585
  }, [apiKey, applicationId, environment, resolvedGatewayUrl, debug]);
7586
+ const [fetchedPolicy, setFetchedPolicy] = React.useState(void 0);
7587
+ React.useEffect(() => {
7588
+ if (mediaPolicy) return;
7589
+ let mounted = true;
7590
+ void (async () => {
7591
+ try {
7592
+ const fn = baseClient.storage.getPolicy;
7593
+ if (typeof fn !== "function") return;
7594
+ const r = await fn.call(baseClient.storage);
7595
+ if (!mounted) return;
7596
+ const v = r?.data?.media_policy;
7597
+ if (v === "fast_trusted" || v === "safe_visible" || v === "safe_public" || v === "moderated" || v === "compliance") {
7598
+ setFetchedPolicy(v);
7599
+ }
7600
+ } catch {
7601
+ }
7602
+ })();
7603
+ return () => {
7604
+ mounted = false;
7605
+ };
7606
+ }, [baseClient, mediaPolicy]);
7607
+ const effectiveMediaPolicy = mediaPolicy ?? fetchedPolicy;
7572
7608
  React.useEffect(() => {
7573
7609
  const token = client.getSessionToken();
7574
7610
  if (token) {
@@ -7672,7 +7708,7 @@ function ScaleMuleProvider({
7672
7708
  photo: baseClient.photo,
7673
7709
  video: baseClient.video,
7674
7710
  audio: baseClient.audio,
7675
- mediaPolicy,
7711
+ mediaPolicy: effectiveMediaPolicy,
7676
7712
  user,
7677
7713
  setUser: handleSetUser,
7678
7714
  initializing,
@@ -7687,7 +7723,7 @@ function ScaleMuleProvider({
7687
7723
  accountSwitcherPrivacy,
7688
7724
  bootstrapFlags
7689
7725
  }),
7690
- [client, money$1, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags, mediaPolicy]
7726
+ [client, money$1, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags, effectiveMediaPolicy]
7691
7727
  );
7692
7728
  return /* @__PURE__ */ jsxRuntime.jsx(ScaleMuleContext.Provider, { value, children });
7693
7729
  }
@@ -9103,10 +9139,13 @@ function useMedia() {
9103
9139
  async (fileId) => {
9104
9140
  setError(null);
9105
9141
  try {
9106
- const r = await storage.delete(fileId);
9107
- if (r.error) {
9108
- if (r.error.status === 404) return;
9109
- throw r.error;
9142
+ const [, storageResult] = await Promise.all([
9143
+ photo.delete(fileId).catch(() => void 0),
9144
+ storage.delete(fileId)
9145
+ ]);
9146
+ if (storageResult.error) {
9147
+ if (storageResult.error.status === 404) return;
9148
+ throw storageResult.error;
9110
9149
  }
9111
9150
  } catch (err) {
9112
9151
  const e = err;
@@ -9114,7 +9153,7 @@ function useMedia() {
9114
9153
  throw e;
9115
9154
  }
9116
9155
  },
9117
- [storage]
9156
+ [storage, photo]
9118
9157
  );
9119
9158
  return { upload, cancelUpload, error, uploading };
9120
9159
  }
@@ -9171,13 +9210,26 @@ function useFileStatus(options) {
9171
9210
  }, [fetchStatus]);
9172
9211
  React.useEffect(() => {
9173
9212
  if (!pollIntervalMs || disabled || !fileId) return;
9174
- const isClean = status?.scan.status === "clean";
9175
- if (isClean) return;
9213
+ const scan = status?.scan.status;
9214
+ if (scan === "threat" || scan === "quarantined" || scan === "error") return;
9215
+ const optimizePending = status?.optimize != null && status.optimize.status !== "done";
9216
+ const transcodePending = status?.transcode != null && status.transcode.status !== "done";
9217
+ const pipelinePending = optimizePending || transcodePending;
9218
+ const isSettled = scan === "clean" && !pipelinePending;
9219
+ if (isSettled) return;
9176
9220
  const id = setInterval(() => {
9177
9221
  void fetchStatus();
9178
9222
  }, pollIntervalMs);
9179
9223
  return () => clearInterval(id);
9180
- }, [pollIntervalMs, disabled, fileId, status?.scan.status, fetchStatus]);
9224
+ }, [
9225
+ pollIntervalMs,
9226
+ disabled,
9227
+ fileId,
9228
+ status?.scan.status,
9229
+ status?.optimize,
9230
+ status?.transcode,
9231
+ fetchStatus
9232
+ ]);
9181
9233
  React.useEffect(() => {
9182
9234
  if (!conversationId || !fileId || disabled) return;
9183
9235
  const channel = conversationChannel(conversationKind, conversationId);
@@ -9212,33 +9264,48 @@ function ScaleMuleMedia(props) {
9212
9264
  renderBlocked,
9213
9265
  renderOverride
9214
9266
  } = props;
9267
+ const { gatewayUrl } = useScaleMule();
9215
9268
  const { status, isReady } = useFileStatus({
9216
9269
  fileId,
9217
9270
  pollIntervalMs,
9218
9271
  conversationId,
9219
9272
  conversationKind
9220
9273
  });
9274
+ const absoluteUrl = React__namespace.useCallback(
9275
+ (url) => {
9276
+ if (!url) return null;
9277
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("blob:") || url.startsWith("data:")) {
9278
+ return url;
9279
+ }
9280
+ if (!gatewayUrl) return url;
9281
+ const base = gatewayUrl.endsWith("/") ? gatewayUrl.slice(0, -1) : gatewayUrl;
9282
+ const path = url.startsWith("/") ? url : `/${url}`;
9283
+ return `${base}${path}`;
9284
+ },
9285
+ [gatewayUrl]
9286
+ );
9221
9287
  const isImage = mimeType.startsWith("image/");
9222
9288
  const isVideo = mimeType.startsWith("video/");
9223
9289
  const isAudio = mimeType.startsWith("audio/");
9224
9290
  const src = React.useMemo(() => {
9225
9291
  const scan = status?.scan.status;
9226
9292
  if (scan === "threat" || scan === "quarantined") return null;
9227
- if (status && isReady) {
9228
- if (isImage && status.urls.optimized) return status.urls.optimized;
9229
- if (isVideo && status.urls.hls) return status.urls.hls;
9230
- return status.urls.original ?? null;
9231
- }
9232
- if (blobPreview) return blobPreview;
9233
- return null;
9234
- }, [status, isReady, isImage, isVideo, blobPreview]);
9293
+ if (!status) {
9294
+ return blobPreview ?? null;
9295
+ }
9296
+ if (isImage && status.urls.optimized) return absoluteUrl(status.urls.optimized);
9297
+ if (isVideo && status.urls.hls) return absoluteUrl(status.urls.hls);
9298
+ if (status.urls.original) return absoluteUrl(status.urls.original);
9299
+ return blobPreview ?? null;
9300
+ }, [status, isImage, isVideo, blobPreview, absoluteUrl]);
9235
9301
  const renderState = React.useMemo(() => {
9236
9302
  const scan = status?.scan.status;
9237
9303
  if (scan === "threat" || scan === "quarantined") return "blocked";
9304
+ if (status?.urls.original) return "ready";
9238
9305
  if (isReady) return "ready";
9239
9306
  if (blobPreview) return "preview";
9240
9307
  return "pending";
9241
- }, [status?.scan.status, isReady, blobPreview]);
9308
+ }, [status?.scan.status, status?.urls.original, isReady, blobPreview]);
9242
9309
  const videoRef = React__namespace.useRef(null);
9243
9310
  React.useEffect(() => {
9244
9311
  if (!isVideo) return;
@@ -9279,9 +9346,6 @@ function ScaleMuleMedia(props) {
9279
9346
  if (hls) hls.destroy();
9280
9347
  };
9281
9348
  }, [isVideo, src]);
9282
- if (renderOverride) {
9283
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderOverride({ src, state: renderState }) });
9284
- }
9285
9349
  if (renderState === "blocked") {
9286
9350
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderBlocked ? renderBlocked() : /* @__PURE__ */ jsxRuntime.jsx(
9287
9351
  "div",
@@ -9293,6 +9357,9 @@ function ScaleMuleMedia(props) {
9293
9357
  }
9294
9358
  ) });
9295
9359
  }
9360
+ if (renderOverride) {
9361
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderOverride({ src, state: renderState }) });
9362
+ }
9296
9363
  if (renderState === "pending" && !src) {
9297
9364
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderPlaceholder ? renderPlaceholder() : /* @__PURE__ */ jsxRuntime.jsx(
9298
9365
  "div",
package/dist/index.mjs CHANGED
@@ -2751,6 +2751,20 @@ var StorageService = class extends ServiceModule {
2751
2751
  async getFileStatus(fileId, options) {
2752
2752
  return this._get(`/files/${fileId}/status`, options);
2753
2753
  }
2754
+ /**
2755
+ * Read the application's active media policy. Lightweight endpoint
2756
+ * (`GET /v1/storage/policy`) used by the SDK on boot to pick up the
2757
+ * platform-default `media_policy` without requiring app-admin auth.
2758
+ *
2759
+ * @example
2760
+ * ```ts
2761
+ * const { data } = await client.storage.getPolicy();
2762
+ * console.log(data?.media_policy); // 'safe_visible'
2763
+ * ```
2764
+ */
2765
+ async getPolicy(options) {
2766
+ return this._get(`/policy`, options);
2767
+ }
2754
2768
  /**
2755
2769
  * Get a signed view URL for inline display (img src, thumbnails).
2756
2770
  * Returns CloudFront signed URL (fast, ~1us) or S3 presigned fallback.
@@ -7549,6 +7563,28 @@ function ScaleMuleProvider({
7549
7563
  debug
7550
7564
  });
7551
7565
  }, [apiKey, applicationId, environment, resolvedGatewayUrl, debug]);
7566
+ const [fetchedPolicy, setFetchedPolicy] = useState(void 0);
7567
+ useEffect(() => {
7568
+ if (mediaPolicy) return;
7569
+ let mounted = true;
7570
+ void (async () => {
7571
+ try {
7572
+ const fn = baseClient.storage.getPolicy;
7573
+ if (typeof fn !== "function") return;
7574
+ const r = await fn.call(baseClient.storage);
7575
+ if (!mounted) return;
7576
+ const v = r?.data?.media_policy;
7577
+ if (v === "fast_trusted" || v === "safe_visible" || v === "safe_public" || v === "moderated" || v === "compliance") {
7578
+ setFetchedPolicy(v);
7579
+ }
7580
+ } catch {
7581
+ }
7582
+ })();
7583
+ return () => {
7584
+ mounted = false;
7585
+ };
7586
+ }, [baseClient, mediaPolicy]);
7587
+ const effectiveMediaPolicy = mediaPolicy ?? fetchedPolicy;
7552
7588
  useEffect(() => {
7553
7589
  const token = client.getSessionToken();
7554
7590
  if (token) {
@@ -7652,7 +7688,7 @@ function ScaleMuleProvider({
7652
7688
  photo: baseClient.photo,
7653
7689
  video: baseClient.video,
7654
7690
  audio: baseClient.audio,
7655
- mediaPolicy,
7691
+ mediaPolicy: effectiveMediaPolicy,
7656
7692
  user,
7657
7693
  setUser: handleSetUser,
7658
7694
  initializing,
@@ -7667,7 +7703,7 @@ function ScaleMuleProvider({
7667
7703
  accountSwitcherPrivacy,
7668
7704
  bootstrapFlags
7669
7705
  }),
7670
- [client, money, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags, mediaPolicy]
7706
+ [client, money, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags, effectiveMediaPolicy]
7671
7707
  );
7672
7708
  return /* @__PURE__ */ jsx(ScaleMuleContext.Provider, { value, children });
7673
7709
  }
@@ -9083,10 +9119,13 @@ function useMedia() {
9083
9119
  async (fileId) => {
9084
9120
  setError(null);
9085
9121
  try {
9086
- const r = await storage.delete(fileId);
9087
- if (r.error) {
9088
- if (r.error.status === 404) return;
9089
- throw r.error;
9122
+ const [, storageResult] = await Promise.all([
9123
+ photo.delete(fileId).catch(() => void 0),
9124
+ storage.delete(fileId)
9125
+ ]);
9126
+ if (storageResult.error) {
9127
+ if (storageResult.error.status === 404) return;
9128
+ throw storageResult.error;
9090
9129
  }
9091
9130
  } catch (err) {
9092
9131
  const e = err;
@@ -9094,7 +9133,7 @@ function useMedia() {
9094
9133
  throw e;
9095
9134
  }
9096
9135
  },
9097
- [storage]
9136
+ [storage, photo]
9098
9137
  );
9099
9138
  return { upload, cancelUpload, error, uploading };
9100
9139
  }
@@ -9151,13 +9190,26 @@ function useFileStatus(options) {
9151
9190
  }, [fetchStatus]);
9152
9191
  useEffect(() => {
9153
9192
  if (!pollIntervalMs || disabled || !fileId) return;
9154
- const isClean = status?.scan.status === "clean";
9155
- if (isClean) return;
9193
+ const scan = status?.scan.status;
9194
+ if (scan === "threat" || scan === "quarantined" || scan === "error") return;
9195
+ const optimizePending = status?.optimize != null && status.optimize.status !== "done";
9196
+ const transcodePending = status?.transcode != null && status.transcode.status !== "done";
9197
+ const pipelinePending = optimizePending || transcodePending;
9198
+ const isSettled = scan === "clean" && !pipelinePending;
9199
+ if (isSettled) return;
9156
9200
  const id = setInterval(() => {
9157
9201
  void fetchStatus();
9158
9202
  }, pollIntervalMs);
9159
9203
  return () => clearInterval(id);
9160
- }, [pollIntervalMs, disabled, fileId, status?.scan.status, fetchStatus]);
9204
+ }, [
9205
+ pollIntervalMs,
9206
+ disabled,
9207
+ fileId,
9208
+ status?.scan.status,
9209
+ status?.optimize,
9210
+ status?.transcode,
9211
+ fetchStatus
9212
+ ]);
9161
9213
  useEffect(() => {
9162
9214
  if (!conversationId || !fileId || disabled) return;
9163
9215
  const channel = conversationChannel(conversationKind, conversationId);
@@ -9192,33 +9244,48 @@ function ScaleMuleMedia(props) {
9192
9244
  renderBlocked,
9193
9245
  renderOverride
9194
9246
  } = props;
9247
+ const { gatewayUrl } = useScaleMule();
9195
9248
  const { status, isReady } = useFileStatus({
9196
9249
  fileId,
9197
9250
  pollIntervalMs,
9198
9251
  conversationId,
9199
9252
  conversationKind
9200
9253
  });
9254
+ const absoluteUrl = React.useCallback(
9255
+ (url) => {
9256
+ if (!url) return null;
9257
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("blob:") || url.startsWith("data:")) {
9258
+ return url;
9259
+ }
9260
+ if (!gatewayUrl) return url;
9261
+ const base = gatewayUrl.endsWith("/") ? gatewayUrl.slice(0, -1) : gatewayUrl;
9262
+ const path = url.startsWith("/") ? url : `/${url}`;
9263
+ return `${base}${path}`;
9264
+ },
9265
+ [gatewayUrl]
9266
+ );
9201
9267
  const isImage = mimeType.startsWith("image/");
9202
9268
  const isVideo = mimeType.startsWith("video/");
9203
9269
  const isAudio = mimeType.startsWith("audio/");
9204
9270
  const src = useMemo(() => {
9205
9271
  const scan = status?.scan.status;
9206
9272
  if (scan === "threat" || scan === "quarantined") return null;
9207
- if (status && isReady) {
9208
- if (isImage && status.urls.optimized) return status.urls.optimized;
9209
- if (isVideo && status.urls.hls) return status.urls.hls;
9210
- return status.urls.original ?? null;
9211
- }
9212
- if (blobPreview) return blobPreview;
9213
- return null;
9214
- }, [status, isReady, isImage, isVideo, blobPreview]);
9273
+ if (!status) {
9274
+ return blobPreview ?? null;
9275
+ }
9276
+ if (isImage && status.urls.optimized) return absoluteUrl(status.urls.optimized);
9277
+ if (isVideo && status.urls.hls) return absoluteUrl(status.urls.hls);
9278
+ if (status.urls.original) return absoluteUrl(status.urls.original);
9279
+ return blobPreview ?? null;
9280
+ }, [status, isImage, isVideo, blobPreview, absoluteUrl]);
9215
9281
  const renderState = useMemo(() => {
9216
9282
  const scan = status?.scan.status;
9217
9283
  if (scan === "threat" || scan === "quarantined") return "blocked";
9284
+ if (status?.urls.original) return "ready";
9218
9285
  if (isReady) return "ready";
9219
9286
  if (blobPreview) return "preview";
9220
9287
  return "pending";
9221
- }, [status?.scan.status, isReady, blobPreview]);
9288
+ }, [status?.scan.status, status?.urls.original, isReady, blobPreview]);
9222
9289
  const videoRef = React.useRef(null);
9223
9290
  useEffect(() => {
9224
9291
  if (!isVideo) return;
@@ -9259,9 +9326,6 @@ function ScaleMuleMedia(props) {
9259
9326
  if (hls) hls.destroy();
9260
9327
  };
9261
9328
  }, [isVideo, src]);
9262
- if (renderOverride) {
9263
- return /* @__PURE__ */ jsx(Fragment, { children: renderOverride({ src, state: renderState }) });
9264
- }
9265
9329
  if (renderState === "blocked") {
9266
9330
  return /* @__PURE__ */ jsx(Fragment, { children: renderBlocked ? renderBlocked() : /* @__PURE__ */ jsx(
9267
9331
  "div",
@@ -9273,6 +9337,9 @@ function ScaleMuleMedia(props) {
9273
9337
  }
9274
9338
  ) });
9275
9339
  }
9340
+ if (renderOverride) {
9341
+ return /* @__PURE__ */ jsx(Fragment, { children: renderOverride({ src, state: renderState }) });
9342
+ }
9276
9343
  if (renderState === "pending" && !src) {
9277
9344
  return /* @__PURE__ */ jsx(Fragment, { children: renderPlaceholder ? renderPlaceholder() : /* @__PURE__ */ jsx(
9278
9345
  "div",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/nextjs",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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.43"
57
+ "@scalemule/sdk": ">=0.0.45 <0.1.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "next": ">=14.0.0",