@scalemule/nextjs 0.1.9 → 0.1.10

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
@@ -7569,6 +7569,28 @@ function ScaleMuleProvider({
7569
7569
  debug
7570
7570
  });
7571
7571
  }, [apiKey, applicationId, environment, resolvedGatewayUrl, debug]);
7572
+ const [fetchedPolicy, setFetchedPolicy] = React.useState(void 0);
7573
+ React.useEffect(() => {
7574
+ if (mediaPolicy) return;
7575
+ let mounted = true;
7576
+ void (async () => {
7577
+ try {
7578
+ const fn = baseClient.storage.getPolicy;
7579
+ if (typeof fn !== "function") return;
7580
+ const r = await fn.call(baseClient.storage);
7581
+ if (!mounted) return;
7582
+ const v = r?.data?.media_policy;
7583
+ if (v === "fast_trusted" || v === "safe_visible" || v === "safe_public" || v === "moderated" || v === "compliance") {
7584
+ setFetchedPolicy(v);
7585
+ }
7586
+ } catch {
7587
+ }
7588
+ })();
7589
+ return () => {
7590
+ mounted = false;
7591
+ };
7592
+ }, [baseClient, mediaPolicy]);
7593
+ const effectiveMediaPolicy = mediaPolicy ?? fetchedPolicy;
7572
7594
  React.useEffect(() => {
7573
7595
  const token = client.getSessionToken();
7574
7596
  if (token) {
@@ -7672,7 +7694,7 @@ function ScaleMuleProvider({
7672
7694
  photo: baseClient.photo,
7673
7695
  video: baseClient.video,
7674
7696
  audio: baseClient.audio,
7675
- mediaPolicy,
7697
+ mediaPolicy: effectiveMediaPolicy,
7676
7698
  user,
7677
7699
  setUser: handleSetUser,
7678
7700
  initializing,
@@ -7687,7 +7709,7 @@ function ScaleMuleProvider({
7687
7709
  accountSwitcherPrivacy,
7688
7710
  bootstrapFlags
7689
7711
  }),
7690
- [client, money$1, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags, mediaPolicy]
7712
+ [client, money$1, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags, effectiveMediaPolicy]
7691
7713
  );
7692
7714
  return /* @__PURE__ */ jsxRuntime.jsx(ScaleMuleContext.Provider, { value, children });
7693
7715
  }
@@ -9103,10 +9125,13 @@ function useMedia() {
9103
9125
  async (fileId) => {
9104
9126
  setError(null);
9105
9127
  try {
9106
- const r = await storage.delete(fileId);
9107
- if (r.error) {
9108
- if (r.error.status === 404) return;
9109
- throw r.error;
9128
+ const [, storageResult] = await Promise.all([
9129
+ photo.delete(fileId).catch(() => void 0),
9130
+ storage.delete(fileId)
9131
+ ]);
9132
+ if (storageResult.error) {
9133
+ if (storageResult.error.status === 404) return;
9134
+ throw storageResult.error;
9110
9135
  }
9111
9136
  } catch (err) {
9112
9137
  const e = err;
@@ -9114,7 +9139,7 @@ function useMedia() {
9114
9139
  throw e;
9115
9140
  }
9116
9141
  },
9117
- [storage]
9142
+ [storage, photo]
9118
9143
  );
9119
9144
  return { upload, cancelUpload, error, uploading };
9120
9145
  }
@@ -9171,13 +9196,26 @@ function useFileStatus(options) {
9171
9196
  }, [fetchStatus]);
9172
9197
  React.useEffect(() => {
9173
9198
  if (!pollIntervalMs || disabled || !fileId) return;
9174
- const isClean = status?.scan.status === "clean";
9175
- if (isClean) return;
9199
+ const scan = status?.scan.status;
9200
+ if (scan === "threat" || scan === "quarantined" || scan === "error") return;
9201
+ const optimizePending = status?.optimize != null && status.optimize.status !== "done";
9202
+ const transcodePending = status?.transcode != null && status.transcode.status !== "done";
9203
+ const pipelinePending = optimizePending || transcodePending;
9204
+ const isSettled = scan === "clean" && !pipelinePending;
9205
+ if (isSettled) return;
9176
9206
  const id = setInterval(() => {
9177
9207
  void fetchStatus();
9178
9208
  }, pollIntervalMs);
9179
9209
  return () => clearInterval(id);
9180
- }, [pollIntervalMs, disabled, fileId, status?.scan.status, fetchStatus]);
9210
+ }, [
9211
+ pollIntervalMs,
9212
+ disabled,
9213
+ fileId,
9214
+ status?.scan.status,
9215
+ status?.optimize,
9216
+ status?.transcode,
9217
+ fetchStatus
9218
+ ]);
9181
9219
  React.useEffect(() => {
9182
9220
  if (!conversationId || !fileId || disabled) return;
9183
9221
  const channel = conversationChannel(conversationKind, conversationId);
@@ -9212,33 +9250,48 @@ function ScaleMuleMedia(props) {
9212
9250
  renderBlocked,
9213
9251
  renderOverride
9214
9252
  } = props;
9253
+ const { gatewayUrl } = useScaleMule();
9215
9254
  const { status, isReady } = useFileStatus({
9216
9255
  fileId,
9217
9256
  pollIntervalMs,
9218
9257
  conversationId,
9219
9258
  conversationKind
9220
9259
  });
9260
+ const absoluteUrl = React__namespace.useCallback(
9261
+ (url) => {
9262
+ if (!url) return null;
9263
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("blob:") || url.startsWith("data:")) {
9264
+ return url;
9265
+ }
9266
+ if (!gatewayUrl) return url;
9267
+ const base = gatewayUrl.endsWith("/") ? gatewayUrl.slice(0, -1) : gatewayUrl;
9268
+ const path = url.startsWith("/") ? url : `/${url}`;
9269
+ return `${base}${path}`;
9270
+ },
9271
+ [gatewayUrl]
9272
+ );
9221
9273
  const isImage = mimeType.startsWith("image/");
9222
9274
  const isVideo = mimeType.startsWith("video/");
9223
9275
  const isAudio = mimeType.startsWith("audio/");
9224
9276
  const src = React.useMemo(() => {
9225
9277
  const scan = status?.scan.status;
9226
9278
  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]);
9279
+ if (!status) {
9280
+ return blobPreview ?? null;
9281
+ }
9282
+ if (isImage && status.urls.optimized) return absoluteUrl(status.urls.optimized);
9283
+ if (isVideo && status.urls.hls) return absoluteUrl(status.urls.hls);
9284
+ if (status.urls.original) return absoluteUrl(status.urls.original);
9285
+ return blobPreview ?? null;
9286
+ }, [status, isImage, isVideo, blobPreview, absoluteUrl]);
9235
9287
  const renderState = React.useMemo(() => {
9236
9288
  const scan = status?.scan.status;
9237
9289
  if (scan === "threat" || scan === "quarantined") return "blocked";
9290
+ if (status?.urls.original) return "ready";
9238
9291
  if (isReady) return "ready";
9239
9292
  if (blobPreview) return "preview";
9240
9293
  return "pending";
9241
- }, [status?.scan.status, isReady, blobPreview]);
9294
+ }, [status?.scan.status, status?.urls.original, isReady, blobPreview]);
9242
9295
  const videoRef = React__namespace.useRef(null);
9243
9296
  React.useEffect(() => {
9244
9297
  if (!isVideo) return;
package/dist/index.mjs CHANGED
@@ -7549,6 +7549,28 @@ function ScaleMuleProvider({
7549
7549
  debug
7550
7550
  });
7551
7551
  }, [apiKey, applicationId, environment, resolvedGatewayUrl, debug]);
7552
+ const [fetchedPolicy, setFetchedPolicy] = useState(void 0);
7553
+ useEffect(() => {
7554
+ if (mediaPolicy) return;
7555
+ let mounted = true;
7556
+ void (async () => {
7557
+ try {
7558
+ const fn = baseClient.storage.getPolicy;
7559
+ if (typeof fn !== "function") return;
7560
+ const r = await fn.call(baseClient.storage);
7561
+ if (!mounted) return;
7562
+ const v = r?.data?.media_policy;
7563
+ if (v === "fast_trusted" || v === "safe_visible" || v === "safe_public" || v === "moderated" || v === "compliance") {
7564
+ setFetchedPolicy(v);
7565
+ }
7566
+ } catch {
7567
+ }
7568
+ })();
7569
+ return () => {
7570
+ mounted = false;
7571
+ };
7572
+ }, [baseClient, mediaPolicy]);
7573
+ const effectiveMediaPolicy = mediaPolicy ?? fetchedPolicy;
7552
7574
  useEffect(() => {
7553
7575
  const token = client.getSessionToken();
7554
7576
  if (token) {
@@ -7652,7 +7674,7 @@ function ScaleMuleProvider({
7652
7674
  photo: baseClient.photo,
7653
7675
  video: baseClient.video,
7654
7676
  audio: baseClient.audio,
7655
- mediaPolicy,
7677
+ mediaPolicy: effectiveMediaPolicy,
7656
7678
  user,
7657
7679
  setUser: handleSetUser,
7658
7680
  initializing,
@@ -7667,7 +7689,7 @@ function ScaleMuleProvider({
7667
7689
  accountSwitcherPrivacy,
7668
7690
  bootstrapFlags
7669
7691
  }),
7670
- [client, money, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags, mediaPolicy]
7692
+ [client, money, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags, effectiveMediaPolicy]
7671
7693
  );
7672
7694
  return /* @__PURE__ */ jsx(ScaleMuleContext.Provider, { value, children });
7673
7695
  }
@@ -9083,10 +9105,13 @@ function useMedia() {
9083
9105
  async (fileId) => {
9084
9106
  setError(null);
9085
9107
  try {
9086
- const r = await storage.delete(fileId);
9087
- if (r.error) {
9088
- if (r.error.status === 404) return;
9089
- throw r.error;
9108
+ const [, storageResult] = await Promise.all([
9109
+ photo.delete(fileId).catch(() => void 0),
9110
+ storage.delete(fileId)
9111
+ ]);
9112
+ if (storageResult.error) {
9113
+ if (storageResult.error.status === 404) return;
9114
+ throw storageResult.error;
9090
9115
  }
9091
9116
  } catch (err) {
9092
9117
  const e = err;
@@ -9094,7 +9119,7 @@ function useMedia() {
9094
9119
  throw e;
9095
9120
  }
9096
9121
  },
9097
- [storage]
9122
+ [storage, photo]
9098
9123
  );
9099
9124
  return { upload, cancelUpload, error, uploading };
9100
9125
  }
@@ -9151,13 +9176,26 @@ function useFileStatus(options) {
9151
9176
  }, [fetchStatus]);
9152
9177
  useEffect(() => {
9153
9178
  if (!pollIntervalMs || disabled || !fileId) return;
9154
- const isClean = status?.scan.status === "clean";
9155
- if (isClean) return;
9179
+ const scan = status?.scan.status;
9180
+ if (scan === "threat" || scan === "quarantined" || scan === "error") return;
9181
+ const optimizePending = status?.optimize != null && status.optimize.status !== "done";
9182
+ const transcodePending = status?.transcode != null && status.transcode.status !== "done";
9183
+ const pipelinePending = optimizePending || transcodePending;
9184
+ const isSettled = scan === "clean" && !pipelinePending;
9185
+ if (isSettled) return;
9156
9186
  const id = setInterval(() => {
9157
9187
  void fetchStatus();
9158
9188
  }, pollIntervalMs);
9159
9189
  return () => clearInterval(id);
9160
- }, [pollIntervalMs, disabled, fileId, status?.scan.status, fetchStatus]);
9190
+ }, [
9191
+ pollIntervalMs,
9192
+ disabled,
9193
+ fileId,
9194
+ status?.scan.status,
9195
+ status?.optimize,
9196
+ status?.transcode,
9197
+ fetchStatus
9198
+ ]);
9161
9199
  useEffect(() => {
9162
9200
  if (!conversationId || !fileId || disabled) return;
9163
9201
  const channel = conversationChannel(conversationKind, conversationId);
@@ -9192,33 +9230,48 @@ function ScaleMuleMedia(props) {
9192
9230
  renderBlocked,
9193
9231
  renderOverride
9194
9232
  } = props;
9233
+ const { gatewayUrl } = useScaleMule();
9195
9234
  const { status, isReady } = useFileStatus({
9196
9235
  fileId,
9197
9236
  pollIntervalMs,
9198
9237
  conversationId,
9199
9238
  conversationKind
9200
9239
  });
9240
+ const absoluteUrl = React.useCallback(
9241
+ (url) => {
9242
+ if (!url) return null;
9243
+ if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("blob:") || url.startsWith("data:")) {
9244
+ return url;
9245
+ }
9246
+ if (!gatewayUrl) return url;
9247
+ const base = gatewayUrl.endsWith("/") ? gatewayUrl.slice(0, -1) : gatewayUrl;
9248
+ const path = url.startsWith("/") ? url : `/${url}`;
9249
+ return `${base}${path}`;
9250
+ },
9251
+ [gatewayUrl]
9252
+ );
9201
9253
  const isImage = mimeType.startsWith("image/");
9202
9254
  const isVideo = mimeType.startsWith("video/");
9203
9255
  const isAudio = mimeType.startsWith("audio/");
9204
9256
  const src = useMemo(() => {
9205
9257
  const scan = status?.scan.status;
9206
9258
  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]);
9259
+ if (!status) {
9260
+ return blobPreview ?? null;
9261
+ }
9262
+ if (isImage && status.urls.optimized) return absoluteUrl(status.urls.optimized);
9263
+ if (isVideo && status.urls.hls) return absoluteUrl(status.urls.hls);
9264
+ if (status.urls.original) return absoluteUrl(status.urls.original);
9265
+ return blobPreview ?? null;
9266
+ }, [status, isImage, isVideo, blobPreview, absoluteUrl]);
9215
9267
  const renderState = useMemo(() => {
9216
9268
  const scan = status?.scan.status;
9217
9269
  if (scan === "threat" || scan === "quarantined") return "blocked";
9270
+ if (status?.urls.original) return "ready";
9218
9271
  if (isReady) return "ready";
9219
9272
  if (blobPreview) return "preview";
9220
9273
  return "pending";
9221
- }, [status?.scan.status, isReady, blobPreview]);
9274
+ }, [status?.scan.status, status?.urls.original, isReady, blobPreview]);
9222
9275
  const videoRef = React.useRef(null);
9223
9276
  useEffect(() => {
9224
9277
  if (!isVideo) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/nextjs",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
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",