@scalemule/nextjs 0.1.29 → 0.1.31
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 +38 -0
- package/dist/client.d.mts +16 -1
- package/dist/client.d.ts +16 -1
- package/dist/client.js +199 -13
- package/dist/client.mjs +199 -13
- package/dist/index-Tq5WdDfS.d.mts +939 -0
- package/dist/index-Tq5WdDfS.d.ts +939 -0
- package/dist/index.d.mts +28 -4
- package/dist/index.d.ts +28 -4
- package/dist/index.js +1268 -43
- package/dist/index.mjs +1269 -45
- package/dist/ledvery-CxPzZpxP.d.mts +44 -0
- package/dist/ledvery-CxPzZpxP.d.ts +44 -0
- package/dist/server/auth.js +6 -1
- package/dist/server/auth.mjs +6 -1
- package/dist/server/index.d.mts +3 -3
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +645 -675
- package/dist/server/index.mjs +646 -676
- package/dist/server/ledvery.d.mts +1 -0
- package/dist/server/ledvery.d.ts +1 -0
- package/dist/server/ledvery.js +336 -0
- package/dist/server/ledvery.mjs +333 -0
- package/dist/server/webhook-handler.d.mts +2 -2
- package/dist/server/webhook-handler.d.ts +2 -2
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/webhook-handler-Cfa7-yKN.d.ts +808 -0
- package/dist/webhook-handler-DN4WBjbm.d.mts +808 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import { createContext, useState, useEffect, useMemo, useCallback, useContext, useRef } from 'react';
|
|
3
3
|
import { createMoneyClient } from '@scalemule/money';
|
|
4
4
|
export { MoneyClient, createMoneyClient } from '@scalemule/money';
|
|
5
|
-
import { jsx,
|
|
5
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
6
|
|
|
7
7
|
var __defProp = Object.defineProperty;
|
|
8
8
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -301,21 +301,93 @@ function buildClientContextHeaders(context) {
|
|
|
301
301
|
if (context.userAgent) headers["X-Client-User-Agent"] = context.userAgent;
|
|
302
302
|
if (context.deviceFingerprint) headers["X-Client-Device-Fingerprint"] = context.deviceFingerprint;
|
|
303
303
|
if (context.referrer) headers["X-Client-Referrer"] = context.referrer;
|
|
304
|
+
if (context.anonymousId) headers["x-anonymous-id"] = context.anonymousId;
|
|
304
305
|
return headers;
|
|
305
306
|
}
|
|
307
|
+
var STORAGE_KEYS = {
|
|
308
|
+
SESSION: "scalemule_session",
|
|
309
|
+
USER_ID: "scalemule_user_id",
|
|
310
|
+
WORKSPACE_ID: "scalemule_workspace_id",
|
|
311
|
+
ANONYMOUS_ID: "scalemule_anonymous_id",
|
|
312
|
+
SESSION_POOL: "scalemule_session_pool",
|
|
313
|
+
ACTIVE_ACCOUNT: "scalemule_active_account",
|
|
314
|
+
KNOWN_ACCOUNTS: "scalemule_known_accounts",
|
|
315
|
+
OFFLINE_QUEUE: "scalemule_offline_queue"
|
|
316
|
+
};
|
|
317
|
+
var LEGACY_ANONYMOUS_ID_KEYS = ["sm_anonymous_id"];
|
|
318
|
+
function generateAnonymousId() {
|
|
319
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
320
|
+
return crypto.randomUUID();
|
|
321
|
+
}
|
|
322
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
323
|
+
const r = Math.random() * 16 | 0;
|
|
324
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
325
|
+
return v.toString(16);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
var inFlight = /* @__PURE__ */ new WeakMap();
|
|
329
|
+
function ensureAnonymousId(storage) {
|
|
330
|
+
const existing = inFlight.get(storage);
|
|
331
|
+
if (existing) return existing;
|
|
332
|
+
const promise = (async () => {
|
|
333
|
+
try {
|
|
334
|
+
return await resolveAnonymousId(storage);
|
|
335
|
+
} finally {
|
|
336
|
+
inFlight.delete(storage);
|
|
337
|
+
}
|
|
338
|
+
})();
|
|
339
|
+
inFlight.set(storage, promise);
|
|
340
|
+
return promise;
|
|
341
|
+
}
|
|
342
|
+
async function resolveAnonymousId(storage) {
|
|
343
|
+
const canonical = await storage.getItem(STORAGE_KEYS.ANONYMOUS_ID);
|
|
344
|
+
if (canonical) {
|
|
345
|
+
for (const legacyKey of LEGACY_ANONYMOUS_ID_KEYS) {
|
|
346
|
+
const legacy = await storage.getItem(legacyKey);
|
|
347
|
+
if (legacy !== canonical) {
|
|
348
|
+
try {
|
|
349
|
+
await storage.setItem(legacyKey, canonical);
|
|
350
|
+
} catch {
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return canonical;
|
|
355
|
+
}
|
|
356
|
+
for (const legacyKey of LEGACY_ANONYMOUS_ID_KEYS) {
|
|
357
|
+
const legacy = await storage.getItem(legacyKey);
|
|
358
|
+
if (legacy) {
|
|
359
|
+
try {
|
|
360
|
+
await storage.setItem(STORAGE_KEYS.ANONYMOUS_ID, legacy);
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
return legacy;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const fresh = generateAnonymousId();
|
|
367
|
+
try {
|
|
368
|
+
await storage.setItem(STORAGE_KEYS.ANONYMOUS_ID, fresh);
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
for (const legacyKey of LEGACY_ANONYMOUS_ID_KEYS) {
|
|
372
|
+
try {
|
|
373
|
+
await storage.setItem(legacyKey, fresh);
|
|
374
|
+
} catch {
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return fresh;
|
|
378
|
+
}
|
|
306
379
|
var SDK_VERSION = "0.0.1";
|
|
307
380
|
var DEFAULT_TIMEOUT = 3e4;
|
|
308
381
|
var DEFAULT_MAX_RETRIES = 2;
|
|
309
382
|
var DEFAULT_BACKOFF_MS = 300;
|
|
310
383
|
var MAX_BACKOFF_MS = 3e4;
|
|
311
|
-
var SESSION_STORAGE_KEY =
|
|
312
|
-
var USER_ID_STORAGE_KEY =
|
|
313
|
-
var OFFLINE_QUEUE_KEY =
|
|
314
|
-
var WORKSPACE_STORAGE_KEY =
|
|
315
|
-
var
|
|
316
|
-
var
|
|
317
|
-
var
|
|
318
|
-
var KNOWN_ACCOUNTS_KEY = "scalemule_known_accounts";
|
|
384
|
+
var SESSION_STORAGE_KEY = STORAGE_KEYS.SESSION;
|
|
385
|
+
var USER_ID_STORAGE_KEY = STORAGE_KEYS.USER_ID;
|
|
386
|
+
var OFFLINE_QUEUE_KEY = STORAGE_KEYS.OFFLINE_QUEUE;
|
|
387
|
+
var WORKSPACE_STORAGE_KEY = STORAGE_KEYS.WORKSPACE_ID;
|
|
388
|
+
var SESSION_POOL_KEY = STORAGE_KEYS.SESSION_POOL;
|
|
389
|
+
var ACTIVE_ACCOUNT_KEY = STORAGE_KEYS.ACTIVE_ACCOUNT;
|
|
390
|
+
var KNOWN_ACCOUNTS_KEY = STORAGE_KEYS.KNOWN_ACCOUNTS;
|
|
319
391
|
var GATEWAY_URLS = {
|
|
320
392
|
dev: "https://api-dev.scalemule.com",
|
|
321
393
|
prod: "https://api.scalemule.com"
|
|
@@ -558,6 +630,7 @@ var ScaleMuleClient = class {
|
|
|
558
630
|
this.offlineQueue = null;
|
|
559
631
|
this.workspaceId = null;
|
|
560
632
|
this.anonymousId = null;
|
|
633
|
+
this.anonymousIdPromise = null;
|
|
561
634
|
this.sessionPool = /* @__PURE__ */ new Map();
|
|
562
635
|
this.knownAccounts = /* @__PURE__ */ new Map();
|
|
563
636
|
this.refreshPromise = null;
|
|
@@ -593,12 +666,7 @@ var ScaleMuleClient = class {
|
|
|
593
666
|
if (userId) this.userId = userId;
|
|
594
667
|
const wsId = await this.storage.getItem(WORKSPACE_STORAGE_KEY);
|
|
595
668
|
if (wsId) this.workspaceId = wsId;
|
|
596
|
-
|
|
597
|
-
if (!anonId) {
|
|
598
|
-
anonId = crypto.randomUUID();
|
|
599
|
-
await this.storage.setItem(ANONYMOUS_ID_STORAGE_KEY, anonId);
|
|
600
|
-
}
|
|
601
|
-
this.anonymousId = anonId;
|
|
669
|
+
await this.ensureAnonymousId();
|
|
602
670
|
if (this.multiSessionEnabled) {
|
|
603
671
|
const poolJson = await this.storage.getItem(SESSION_POOL_KEY);
|
|
604
672
|
if (poolJson) {
|
|
@@ -650,7 +718,7 @@ var ScaleMuleClient = class {
|
|
|
650
718
|
"[ScaleMule] Initialized, session:",
|
|
651
719
|
!!this.sessionToken,
|
|
652
720
|
"anonymousId:",
|
|
653
|
-
|
|
721
|
+
this.anonymousId,
|
|
654
722
|
"poolSize:",
|
|
655
723
|
this.sessionPool.size,
|
|
656
724
|
"knownAccounts:",
|
|
@@ -704,9 +772,38 @@ var ScaleMuleClient = class {
|
|
|
704
772
|
isAuthenticated() {
|
|
705
773
|
return this.sessionToken !== null;
|
|
706
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* Sync accessor for the cached anonymous ID. Returns `null` until either
|
|
777
|
+
* `initialize()` has run or `ensureAnonymousId()` has been awaited at
|
|
778
|
+
* least once. Use `ensureAnonymousId()` if you need a guaranteed value.
|
|
779
|
+
*/
|
|
707
780
|
getAnonymousId() {
|
|
708
781
|
return this.anonymousId;
|
|
709
782
|
}
|
|
783
|
+
/**
|
|
784
|
+
* Lazy-mint or cache-then-return the anonymous ID.
|
|
785
|
+
*
|
|
786
|
+
* Always returns a usable string. Callers that need to attach the
|
|
787
|
+
* `x-anonymous-id` header before `initialize()` has completed (rare —
|
|
788
|
+
* usually only the very first unauthenticated request from a freshly
|
|
789
|
+
* constructed client) should `await` this rather than relying on
|
|
790
|
+
* `getAnonymousId()`.
|
|
791
|
+
*
|
|
792
|
+
* Concurrent callers receive the same in-flight promise — no risk of
|
|
793
|
+
* two simultaneous first requests minting two distinct IDs.
|
|
794
|
+
*/
|
|
795
|
+
async ensureAnonymousId() {
|
|
796
|
+
if (this.anonymousId) return this.anonymousId;
|
|
797
|
+
if (!this.anonymousIdPromise) {
|
|
798
|
+
this.anonymousIdPromise = ensureAnonymousId(this.storage).then((id) => {
|
|
799
|
+
this.anonymousId = id;
|
|
800
|
+
return id;
|
|
801
|
+
}).finally(() => {
|
|
802
|
+
this.anonymousIdPromise = null;
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
return this.anonymousIdPromise;
|
|
806
|
+
}
|
|
710
807
|
isMultiSessionEnabled() {
|
|
711
808
|
return this.multiSessionEnabled;
|
|
712
809
|
}
|
|
@@ -882,8 +979,16 @@ var ScaleMuleClient = class {
|
|
|
882
979
|
if (this.workspaceId) {
|
|
883
980
|
headers["x-sm-workspace-id"] = this.workspaceId;
|
|
884
981
|
}
|
|
885
|
-
if (!
|
|
886
|
-
|
|
982
|
+
if (!init.skipAuth && !this.sessionToken) {
|
|
983
|
+
if (!this.anonymousId) {
|
|
984
|
+
try {
|
|
985
|
+
await this.ensureAnonymousId();
|
|
986
|
+
} catch {
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (this.anonymousId) {
|
|
990
|
+
headers["x-anonymous-id"] = this.anonymousId;
|
|
991
|
+
}
|
|
887
992
|
}
|
|
888
993
|
if (attempt > 0 && (method === "POST" || method === "PUT" || method === "PATCH")) {
|
|
889
994
|
if (!idempotencyKey) {
|
|
@@ -5686,6 +5791,15 @@ var PhotoService = class extends ServiceModule {
|
|
|
5686
5791
|
async delete(id, options) {
|
|
5687
5792
|
return this.del(`/${id}`, options);
|
|
5688
5793
|
}
|
|
5794
|
+
async getPresets(options) {
|
|
5795
|
+
return this._get("/presets", options);
|
|
5796
|
+
}
|
|
5797
|
+
async getManifest(id, params, options) {
|
|
5798
|
+
return this._get(this.withQuery(`/${id}/manifest`, params), options);
|
|
5799
|
+
}
|
|
5800
|
+
async getPublicManifest(id, params, options) {
|
|
5801
|
+
return this._get(this.withQuery(`/public/${id}/manifest`, params), options);
|
|
5802
|
+
}
|
|
5689
5803
|
/**
|
|
5690
5804
|
* Build an absolute URL for the on-demand transform endpoint.
|
|
5691
5805
|
*
|
|
@@ -5934,6 +6048,592 @@ var PhotoService = class extends ServiceModule {
|
|
|
5934
6048
|
return this.get(id);
|
|
5935
6049
|
}
|
|
5936
6050
|
};
|
|
6051
|
+
var MEDIA_PRESETS = {
|
|
6052
|
+
hero: { widths: [400, 800, 1200, 1600, 2400] },
|
|
6053
|
+
inline: { widths: [400, 800, 1200] },
|
|
6054
|
+
thumbnail: { widths: [120, 240, 360], fit: "cover" },
|
|
6055
|
+
avatar: { widths: [64, 128, 256], fit: "cover" },
|
|
6056
|
+
logo: { widths: [120, 240, 480] }
|
|
6057
|
+
};
|
|
6058
|
+
var MediaService = class extends ServiceModule {
|
|
6059
|
+
constructor(client, storage, photo, video, audio) {
|
|
6060
|
+
super(client);
|
|
6061
|
+
this.storage = storage;
|
|
6062
|
+
this.photo = photo;
|
|
6063
|
+
this.video = video;
|
|
6064
|
+
this.audio = audio;
|
|
6065
|
+
this.basePath = "";
|
|
6066
|
+
}
|
|
6067
|
+
getPresets() {
|
|
6068
|
+
return MEDIA_PRESETS;
|
|
6069
|
+
}
|
|
6070
|
+
async upload(file, options, requestOptions) {
|
|
6071
|
+
const mimeType = file.type || "application/octet-stream";
|
|
6072
|
+
const kind = detectKind(mimeType);
|
|
6073
|
+
const visibility = resolveVisibility(options);
|
|
6074
|
+
const isPublic = visibility !== "private";
|
|
6075
|
+
const isAnonymous = visibility === "anonymous_visible";
|
|
6076
|
+
const gateOnPipeline = shouldGateOnPipeline(options?.policy);
|
|
6077
|
+
const emit = (phase, progress, fileId) => {
|
|
6078
|
+
options?.onProgress?.({ phase, progress, file_id: fileId });
|
|
6079
|
+
};
|
|
6080
|
+
const uploadOptions = {
|
|
6081
|
+
filename: options?.filename,
|
|
6082
|
+
metadata: options?.metadata,
|
|
6083
|
+
signal: options?.signal,
|
|
6084
|
+
onProgress: (progress) => emit("uploading", progress)
|
|
6085
|
+
};
|
|
6086
|
+
emit("presigning", 0);
|
|
6087
|
+
try {
|
|
6088
|
+
if (isAnonymous) {
|
|
6089
|
+
return await this.uploadAnonymous(file, mimeType, kind, options, requestOptions, uploadOptions, emit);
|
|
6090
|
+
}
|
|
6091
|
+
if (kind === "image" && !options?.skipPhotoRegister) {
|
|
6092
|
+
return await this.uploadImage(
|
|
6093
|
+
file,
|
|
6094
|
+
mimeType,
|
|
6095
|
+
visibility,
|
|
6096
|
+
gateOnPipeline,
|
|
6097
|
+
options,
|
|
6098
|
+
requestOptions,
|
|
6099
|
+
uploadOptions,
|
|
6100
|
+
emit
|
|
6101
|
+
);
|
|
6102
|
+
}
|
|
6103
|
+
if (kind === "video" && visibility === "private") {
|
|
6104
|
+
const result = await this.video.uploadViaStorage(file, uploadOptions, requestOptions);
|
|
6105
|
+
if (result.error || !result.data) {
|
|
6106
|
+
return { data: null, error: result.error };
|
|
6107
|
+
}
|
|
6108
|
+
if (gateOnPipeline) {
|
|
6109
|
+
emit("optimizing", 100, result.data.file_id);
|
|
6110
|
+
await result.data.hls_url_promise;
|
|
6111
|
+
}
|
|
6112
|
+
const info = await this.fetchInfoBestEffort(result.data.file_id, requestOptions);
|
|
6113
|
+
const asset2 = this.buildAsset(
|
|
6114
|
+
info ?? fallbackFileInfo(result.data.file_id, mimeType, visibility, result.data.original_view_url, null),
|
|
6115
|
+
{
|
|
6116
|
+
photoId: null,
|
|
6117
|
+
preset: options?.preset,
|
|
6118
|
+
customWidths: options?.customWidths
|
|
6119
|
+
}
|
|
6120
|
+
);
|
|
6121
|
+
emit("ready", 100, result.data.file_id);
|
|
6122
|
+
return {
|
|
6123
|
+
data: {
|
|
6124
|
+
file_id: result.data.file_id,
|
|
6125
|
+
photo_id: null,
|
|
6126
|
+
original_view_url: result.data.original_view_url,
|
|
6127
|
+
optimized_url_promise: Promise.resolve(null),
|
|
6128
|
+
hls_url_promise: result.data.hls_url_promise,
|
|
6129
|
+
mime_type: mimeType,
|
|
6130
|
+
is_public: false,
|
|
6131
|
+
visibility,
|
|
6132
|
+
cdn_url: null,
|
|
6133
|
+
asset: asset2
|
|
6134
|
+
},
|
|
6135
|
+
error: null
|
|
6136
|
+
};
|
|
6137
|
+
}
|
|
6138
|
+
if (kind === "audio" && visibility === "private") {
|
|
6139
|
+
const result = await this.audio.uploadViaStorage(file, uploadOptions, requestOptions);
|
|
6140
|
+
if (result.error || !result.data) {
|
|
6141
|
+
return { data: null, error: result.error };
|
|
6142
|
+
}
|
|
6143
|
+
const info = await this.fetchInfoBestEffort(result.data.file_id, requestOptions);
|
|
6144
|
+
const asset2 = this.buildAsset(
|
|
6145
|
+
info ?? fallbackFileInfo(result.data.file_id, mimeType, visibility, result.data.original_view_url, null),
|
|
6146
|
+
{
|
|
6147
|
+
photoId: null,
|
|
6148
|
+
preset: options?.preset,
|
|
6149
|
+
customWidths: options?.customWidths
|
|
6150
|
+
}
|
|
6151
|
+
);
|
|
6152
|
+
emit("ready", 100, result.data.file_id);
|
|
6153
|
+
return {
|
|
6154
|
+
data: {
|
|
6155
|
+
file_id: result.data.file_id,
|
|
6156
|
+
photo_id: null,
|
|
6157
|
+
original_view_url: result.data.original_view_url,
|
|
6158
|
+
optimized_url_promise: Promise.resolve(null),
|
|
6159
|
+
hls_url_promise: Promise.resolve(null),
|
|
6160
|
+
mime_type: mimeType,
|
|
6161
|
+
is_public: false,
|
|
6162
|
+
visibility,
|
|
6163
|
+
cdn_url: null,
|
|
6164
|
+
asset: asset2
|
|
6165
|
+
},
|
|
6166
|
+
error: null
|
|
6167
|
+
};
|
|
6168
|
+
}
|
|
6169
|
+
const storageResult = isPublic ? await this.storage.upload(file, {
|
|
6170
|
+
...uploadOptions,
|
|
6171
|
+
visibility: "app_public",
|
|
6172
|
+
skipCompression: true
|
|
6173
|
+
}) : await this.storage.uploadPrivate(file, uploadOptions);
|
|
6174
|
+
if (storageResult.error || !storageResult.data) {
|
|
6175
|
+
return { data: null, error: storageResult.error };
|
|
6176
|
+
}
|
|
6177
|
+
const fileInfo = storageResult.data;
|
|
6178
|
+
const asset = this.buildAsset(fileInfo, {
|
|
6179
|
+
photoId: null,
|
|
6180
|
+
preset: options?.preset,
|
|
6181
|
+
customWidths: options?.customWidths
|
|
6182
|
+
});
|
|
6183
|
+
emit("ready", 100, fileInfo.id);
|
|
6184
|
+
return {
|
|
6185
|
+
data: {
|
|
6186
|
+
file_id: fileInfo.id,
|
|
6187
|
+
photo_id: null,
|
|
6188
|
+
original_view_url: fileInfo.url ?? null,
|
|
6189
|
+
optimized_url_promise: Promise.resolve(null),
|
|
6190
|
+
hls_url_promise: Promise.resolve(null),
|
|
6191
|
+
mime_type: mimeType,
|
|
6192
|
+
is_public: fileInfo.is_public ?? isPublic,
|
|
6193
|
+
visibility: resolveFileVisibility(fileInfo, visibility),
|
|
6194
|
+
cdn_url: fileInfo.cdn_url ?? null,
|
|
6195
|
+
asset
|
|
6196
|
+
},
|
|
6197
|
+
error: null
|
|
6198
|
+
};
|
|
6199
|
+
} catch (error) {
|
|
6200
|
+
return {
|
|
6201
|
+
data: null,
|
|
6202
|
+
error: normalizeThrownError(error)
|
|
6203
|
+
};
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6206
|
+
async get(fileId, options, requestOptions) {
|
|
6207
|
+
const result = await this.storage.getInfo(fileId, requestOptions);
|
|
6208
|
+
if (result.error || !result.data) {
|
|
6209
|
+
return { data: null, error: result.error };
|
|
6210
|
+
}
|
|
6211
|
+
return {
|
|
6212
|
+
data: this.buildAsset(result.data, {
|
|
6213
|
+
photoId: null,
|
|
6214
|
+
preset: options?.preset,
|
|
6215
|
+
customWidths: options?.customWidths
|
|
6216
|
+
}),
|
|
6217
|
+
error: null
|
|
6218
|
+
};
|
|
6219
|
+
}
|
|
6220
|
+
async list(params, requestOptions) {
|
|
6221
|
+
const result = await this.storage.list(params, requestOptions);
|
|
6222
|
+
return {
|
|
6223
|
+
data: result.data.map(
|
|
6224
|
+
(file) => this.buildAsset(file, {
|
|
6225
|
+
photoId: null,
|
|
6226
|
+
preset: params?.preset,
|
|
6227
|
+
customWidths: params?.customWidths
|
|
6228
|
+
})
|
|
6229
|
+
),
|
|
6230
|
+
metadata: result.metadata,
|
|
6231
|
+
error: result.error
|
|
6232
|
+
};
|
|
6233
|
+
}
|
|
6234
|
+
async delete(fileId, options) {
|
|
6235
|
+
try {
|
|
6236
|
+
const [, storageResult] = await Promise.all([
|
|
6237
|
+
this.photo.delete(fileId, options).catch(() => ({ data: null, error: null })),
|
|
6238
|
+
this.storage.delete(fileId, options)
|
|
6239
|
+
]);
|
|
6240
|
+
if (storageResult.error && storageResult.error.status !== 404) {
|
|
6241
|
+
return { data: null, error: storageResult.error };
|
|
6242
|
+
}
|
|
6243
|
+
return { data: { deleted: true }, error: null };
|
|
6244
|
+
} catch (error) {
|
|
6245
|
+
return { data: null, error: normalizeThrownError(error) };
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
6248
|
+
async releaseQuarantine(fileId, reason = "Released via @scalemule/sdk media.releaseQuarantine()", options) {
|
|
6249
|
+
const response = await this.client.post(
|
|
6250
|
+
`/v1/storage/admin/files/${fileId}/scan-override`,
|
|
6251
|
+
{ scan_status: "clean", reason },
|
|
6252
|
+
options
|
|
6253
|
+
);
|
|
6254
|
+
if (response.error) {
|
|
6255
|
+
return { data: null, error: response.error };
|
|
6256
|
+
}
|
|
6257
|
+
return this.get(fileId, void 0, options);
|
|
6258
|
+
}
|
|
6259
|
+
async getManifest(fileId, options, requestOptions) {
|
|
6260
|
+
const file = await this.storage.getInfo(fileId, requestOptions);
|
|
6261
|
+
if (file.error || !file.data) {
|
|
6262
|
+
return { data: null, error: file.error };
|
|
6263
|
+
}
|
|
6264
|
+
const manifest = await this.fetchManifest(file.data, options, requestOptions);
|
|
6265
|
+
return { data: manifest, error: null };
|
|
6266
|
+
}
|
|
6267
|
+
buildAsset(file, options, manifest) {
|
|
6268
|
+
const visibility = resolveFileVisibility(file);
|
|
6269
|
+
const photoId = options?.photoId ?? null;
|
|
6270
|
+
const originalUrl = visibility === "anonymous_visible" ? file.cdn_url ?? file.url ?? null : file.url ?? file.cdn_url ?? null;
|
|
6271
|
+
return {
|
|
6272
|
+
file_id: file.id,
|
|
6273
|
+
photo_id: photoId,
|
|
6274
|
+
filename: file.filename,
|
|
6275
|
+
content_type: file.content_type,
|
|
6276
|
+
size_bytes: file.size_bytes,
|
|
6277
|
+
kind: detectKind(file.content_type),
|
|
6278
|
+
visibility,
|
|
6279
|
+
is_public: file.is_public ?? visibility !== "private",
|
|
6280
|
+
scan_status: file.scan_status,
|
|
6281
|
+
created_at: file.created_at,
|
|
6282
|
+
original_url: originalUrl,
|
|
6283
|
+
cdn_url: file.cdn_url ?? null,
|
|
6284
|
+
manifest: manifest ?? this.buildManifest(file, options)
|
|
6285
|
+
};
|
|
6286
|
+
}
|
|
6287
|
+
buildManifest(file, options) {
|
|
6288
|
+
const kind = detectKind(file.content_type);
|
|
6289
|
+
const visibility = resolveFileVisibility(file);
|
|
6290
|
+
const originalUrl = visibility === "anonymous_visible" ? file.cdn_url ?? file.url ?? null : file.url ?? file.cdn_url ?? null;
|
|
6291
|
+
if (!originalUrl) {
|
|
6292
|
+
return null;
|
|
6293
|
+
}
|
|
6294
|
+
if (kind !== "image" || file.content_type === "image/svg+xml") {
|
|
6295
|
+
return {
|
|
6296
|
+
file_id: file.id,
|
|
6297
|
+
content_type: file.content_type,
|
|
6298
|
+
preset: "original",
|
|
6299
|
+
variants: { original: originalUrl },
|
|
6300
|
+
srcset: null,
|
|
6301
|
+
default: originalUrl
|
|
6302
|
+
};
|
|
6303
|
+
}
|
|
6304
|
+
const preset = options?.preset ?? "inline";
|
|
6305
|
+
const spec = resolvePresetSpec(preset, options?.customWidths);
|
|
6306
|
+
const variants = Object.fromEntries(
|
|
6307
|
+
spec.widths.map((width) => [
|
|
6308
|
+
String(width),
|
|
6309
|
+
visibility === "anonymous_visible" ? this.photo.getPublicTransformUrl(file.id, transformOptions(width, spec.fit)) : this.photo.getTransformUrl(file.id, transformOptions(width, spec.fit))
|
|
6310
|
+
])
|
|
6311
|
+
);
|
|
6312
|
+
const defaultWidth = pickDefaultWidth(spec.widths);
|
|
6313
|
+
const defaultUrl = variants[String(defaultWidth)] ?? Object.values(variants)[0] ?? originalUrl;
|
|
6314
|
+
return {
|
|
6315
|
+
file_id: file.id,
|
|
6316
|
+
content_type: file.content_type,
|
|
6317
|
+
preset,
|
|
6318
|
+
variants,
|
|
6319
|
+
srcset: spec.widths.map((width) => `${variants[String(width)]} ${width}w`).join(", "),
|
|
6320
|
+
default: defaultUrl
|
|
6321
|
+
};
|
|
6322
|
+
}
|
|
6323
|
+
async uploadAnonymous(file, mimeType, kind, options, requestOptions, uploadOptions, emit) {
|
|
6324
|
+
const result = await this.storage.uploadAnonymous(file, uploadOptions);
|
|
6325
|
+
if (result.error || !result.data) {
|
|
6326
|
+
return { data: null, error: result.error };
|
|
6327
|
+
}
|
|
6328
|
+
emit("scanning", 100, result.data.id);
|
|
6329
|
+
const ready = await this.waitForAnonymousReady(result.data.id, options?.signal, emit);
|
|
6330
|
+
if (ready.error) {
|
|
6331
|
+
return { data: null, error: ready.error };
|
|
6332
|
+
}
|
|
6333
|
+
const fileInfo = await this.fetchInfoBestEffort(result.data.id, requestOptions) ?? {
|
|
6334
|
+
...result.data,
|
|
6335
|
+
cdn_url: ready.cdnUrl ?? result.data.cdn_url,
|
|
6336
|
+
scan_status: ready.scanStatus
|
|
6337
|
+
};
|
|
6338
|
+
let manifest = this.buildManifest(fileInfo, {
|
|
6339
|
+
preset: options?.preset,
|
|
6340
|
+
customWidths: options?.customWidths
|
|
6341
|
+
});
|
|
6342
|
+
if (kind === "image" && fileInfo.content_type !== "image/svg+xml" && options?.preset !== "custom") {
|
|
6343
|
+
emit("optimizing", 100, result.data.id);
|
|
6344
|
+
const persistedManifest = await this.waitForAnonymousManifest(fileInfo.id, options, requestOptions, emit);
|
|
6345
|
+
if (persistedManifest.error) {
|
|
6346
|
+
return { data: null, error: persistedManifest.error };
|
|
6347
|
+
}
|
|
6348
|
+
manifest = persistedManifest.manifest ?? manifest;
|
|
6349
|
+
}
|
|
6350
|
+
const asset = this.buildAsset(
|
|
6351
|
+
fileInfo,
|
|
6352
|
+
{
|
|
6353
|
+
photoId: null,
|
|
6354
|
+
preset: options?.preset,
|
|
6355
|
+
customWidths: options?.customWidths
|
|
6356
|
+
},
|
|
6357
|
+
manifest
|
|
6358
|
+
);
|
|
6359
|
+
emit("ready", 100, result.data.id);
|
|
6360
|
+
return {
|
|
6361
|
+
data: {
|
|
6362
|
+
file_id: result.data.id,
|
|
6363
|
+
photo_id: null,
|
|
6364
|
+
original_view_url: fileInfo.cdn_url ?? null,
|
|
6365
|
+
optimized_url_promise: Promise.resolve(manifest?.default ?? null),
|
|
6366
|
+
hls_url_promise: Promise.resolve(null),
|
|
6367
|
+
mime_type: mimeType,
|
|
6368
|
+
is_public: true,
|
|
6369
|
+
visibility: "anonymous_visible",
|
|
6370
|
+
cdn_url: fileInfo.cdn_url ?? null,
|
|
6371
|
+
asset
|
|
6372
|
+
},
|
|
6373
|
+
error: null
|
|
6374
|
+
};
|
|
6375
|
+
}
|
|
6376
|
+
async uploadImage(file, mimeType, visibility, gateOnPipeline, options, requestOptions, uploadOptions, emit) {
|
|
6377
|
+
if (visibility === "private") {
|
|
6378
|
+
const result = await this.photo.uploadViaStorage(file, uploadOptions, requestOptions);
|
|
6379
|
+
if (result.error || !result.data) {
|
|
6380
|
+
return { data: null, error: result.error };
|
|
6381
|
+
}
|
|
6382
|
+
if (gateOnPipeline) {
|
|
6383
|
+
emit("optimizing", 100, result.data.file_id);
|
|
6384
|
+
await result.data.optimized_url_promise;
|
|
6385
|
+
}
|
|
6386
|
+
const info2 = await this.fetchInfoBestEffort(result.data.file_id, requestOptions);
|
|
6387
|
+
const asset2 = this.buildAsset(
|
|
6388
|
+
info2 ?? fallbackFileInfo(result.data.file_id, mimeType, visibility, result.data.original_view_url, null),
|
|
6389
|
+
{
|
|
6390
|
+
photoId: result.data.photo_id,
|
|
6391
|
+
preset: options?.preset,
|
|
6392
|
+
customWidths: options?.customWidths
|
|
6393
|
+
}
|
|
6394
|
+
);
|
|
6395
|
+
emit("ready", 100, result.data.file_id);
|
|
6396
|
+
return {
|
|
6397
|
+
data: {
|
|
6398
|
+
file_id: result.data.file_id,
|
|
6399
|
+
photo_id: result.data.photo_id,
|
|
6400
|
+
original_view_url: result.data.original_view_url,
|
|
6401
|
+
optimized_url_promise: result.data.optimized_url_promise,
|
|
6402
|
+
hls_url_promise: Promise.resolve(null),
|
|
6403
|
+
mime_type: mimeType,
|
|
6404
|
+
is_public: false,
|
|
6405
|
+
visibility,
|
|
6406
|
+
cdn_url: null,
|
|
6407
|
+
asset: asset2
|
|
6408
|
+
},
|
|
6409
|
+
error: null
|
|
6410
|
+
};
|
|
6411
|
+
}
|
|
6412
|
+
const storageResult = await this.storage.upload(file, {
|
|
6413
|
+
...uploadOptions,
|
|
6414
|
+
visibility: "app_public",
|
|
6415
|
+
skipCompression: true
|
|
6416
|
+
});
|
|
6417
|
+
if (storageResult.error || !storageResult.data) {
|
|
6418
|
+
return { data: null, error: storageResult.error };
|
|
6419
|
+
}
|
|
6420
|
+
const originalViewUrl = storageResult.data.url ?? null;
|
|
6421
|
+
const registerResult = await this.photo.register({ fileId: storageResult.data.id }, requestOptions);
|
|
6422
|
+
const photoId = registerResult.error || !registerResult.data ? null : registerResult.data.id;
|
|
6423
|
+
const optimizedUrlPromise = photoId ? this.pollPhotoOptimizationComplete(photoId, requestOptions) : Promise.resolve(null);
|
|
6424
|
+
if (gateOnPipeline) {
|
|
6425
|
+
emit("optimizing", 100, storageResult.data.id);
|
|
6426
|
+
await optimizedUrlPromise;
|
|
6427
|
+
}
|
|
6428
|
+
const info = await this.fetchInfoBestEffort(storageResult.data.id, requestOptions) ?? storageResult.data;
|
|
6429
|
+
const asset = this.buildAsset(info, {
|
|
6430
|
+
photoId,
|
|
6431
|
+
preset: options?.preset,
|
|
6432
|
+
customWidths: options?.customWidths
|
|
6433
|
+
});
|
|
6434
|
+
emit("ready", 100, storageResult.data.id);
|
|
6435
|
+
return {
|
|
6436
|
+
data: {
|
|
6437
|
+
file_id: storageResult.data.id,
|
|
6438
|
+
photo_id: photoId,
|
|
6439
|
+
original_view_url: originalViewUrl,
|
|
6440
|
+
optimized_url_promise: optimizedUrlPromise,
|
|
6441
|
+
hls_url_promise: Promise.resolve(null),
|
|
6442
|
+
mime_type: mimeType,
|
|
6443
|
+
is_public: true,
|
|
6444
|
+
visibility,
|
|
6445
|
+
cdn_url: info.cdn_url ?? null,
|
|
6446
|
+
asset
|
|
6447
|
+
},
|
|
6448
|
+
error: null
|
|
6449
|
+
};
|
|
6450
|
+
}
|
|
6451
|
+
async waitForAnonymousReady(fileId, signal, emit) {
|
|
6452
|
+
const delays = [500, 1e3, 1500, 2e3, 2500, 3e3];
|
|
6453
|
+
for (let attempt = 0; attempt < 24; attempt++) {
|
|
6454
|
+
if (signal?.aborted) {
|
|
6455
|
+
return {
|
|
6456
|
+
cdnUrl: null,
|
|
6457
|
+
scanStatus: void 0,
|
|
6458
|
+
error: { code: "aborted", message: "Upload aborted", status: 0 }
|
|
6459
|
+
};
|
|
6460
|
+
}
|
|
6461
|
+
const status = await this.storage.getFileStatus(fileId);
|
|
6462
|
+
if (status.error || !status.data) {
|
|
6463
|
+
await sleep4(delays[Math.min(attempt, delays.length - 1)] ?? 3e3);
|
|
6464
|
+
continue;
|
|
6465
|
+
}
|
|
6466
|
+
const scanStatus = status.data.scan.status;
|
|
6467
|
+
if (status.data.urls.cdn_url) {
|
|
6468
|
+
return { cdnUrl: status.data.urls.cdn_url, scanStatus, error: null };
|
|
6469
|
+
}
|
|
6470
|
+
if (scanStatus === "threat" || scanStatus === "quarantined" || scanStatus === "error") {
|
|
6471
|
+
return {
|
|
6472
|
+
cdnUrl: null,
|
|
6473
|
+
scanStatus,
|
|
6474
|
+
error: scanError(scanStatus)
|
|
6475
|
+
};
|
|
6476
|
+
}
|
|
6477
|
+
emit("scanning", Math.min(99, 10 + attempt * 4), fileId);
|
|
6478
|
+
await sleep4(delays[Math.min(attempt, delays.length - 1)] ?? 3e3);
|
|
6479
|
+
}
|
|
6480
|
+
return {
|
|
6481
|
+
cdnUrl: null,
|
|
6482
|
+
scanStatus: "pending",
|
|
6483
|
+
error: {
|
|
6484
|
+
code: "file_scanning",
|
|
6485
|
+
message: "File scan did not complete in time. Re-fetch the file status and publish once cdn_url appears.",
|
|
6486
|
+
status: 202
|
|
6487
|
+
}
|
|
6488
|
+
};
|
|
6489
|
+
}
|
|
6490
|
+
async fetchInfoBestEffort(fileId, requestOptions) {
|
|
6491
|
+
const result = await this.storage.getInfo(fileId, requestOptions);
|
|
6492
|
+
return result.error || !result.data ? null : result.data;
|
|
6493
|
+
}
|
|
6494
|
+
async fetchManifest(file, options, requestOptions) {
|
|
6495
|
+
const kind = detectKind(file.content_type);
|
|
6496
|
+
const visibility = resolveFileVisibility(file);
|
|
6497
|
+
if (kind !== "image" || file.content_type === "image/svg+xml") {
|
|
6498
|
+
return this.buildManifest(file, options);
|
|
6499
|
+
}
|
|
6500
|
+
if (visibility !== "anonymous_visible" || options?.preset === "custom") {
|
|
6501
|
+
return this.buildManifest(file, options);
|
|
6502
|
+
}
|
|
6503
|
+
const persisted = await this.photo.getPublicManifest(
|
|
6504
|
+
file.id,
|
|
6505
|
+
{ preset: options?.preset ?? "inline" },
|
|
6506
|
+
requestOptions
|
|
6507
|
+
);
|
|
6508
|
+
if (persisted.error || !persisted.data) {
|
|
6509
|
+
return this.buildManifest(file, options);
|
|
6510
|
+
}
|
|
6511
|
+
return persisted.data;
|
|
6512
|
+
}
|
|
6513
|
+
async pollPhotoOptimizationComplete(photoId, requestOptions) {
|
|
6514
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
6515
|
+
const result = await this.photo.get(photoId, requestOptions);
|
|
6516
|
+
const status = result.data?.optimization_status;
|
|
6517
|
+
if (status === "completed") {
|
|
6518
|
+
return this.photo.getTransformUrl(photoId, { width: 1200 });
|
|
6519
|
+
}
|
|
6520
|
+
await sleep4(250);
|
|
6521
|
+
}
|
|
6522
|
+
return null;
|
|
6523
|
+
}
|
|
6524
|
+
async waitForAnonymousManifest(fileId, options, requestOptions, emit) {
|
|
6525
|
+
const preset = options?.preset ?? "inline";
|
|
6526
|
+
const delays = [500, 1e3, 1500, 2e3, 2500, 3e3];
|
|
6527
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
6528
|
+
if (options?.signal?.aborted) {
|
|
6529
|
+
return {
|
|
6530
|
+
manifest: null,
|
|
6531
|
+
error: { code: "aborted", message: "Upload aborted", status: 0 }
|
|
6532
|
+
};
|
|
6533
|
+
}
|
|
6534
|
+
const manifest = await this.photo.getPublicManifest(fileId, { preset }, requestOptions);
|
|
6535
|
+
if (!manifest.error && manifest.data && manifest.data.ready) {
|
|
6536
|
+
return { manifest: manifest.data, error: null };
|
|
6537
|
+
}
|
|
6538
|
+
emit("optimizing", Math.min(99, 15 + attempt * 3), fileId);
|
|
6539
|
+
await sleep4(delays[Math.min(attempt, delays.length - 1)] ?? 3e3);
|
|
6540
|
+
}
|
|
6541
|
+
return {
|
|
6542
|
+
manifest: null,
|
|
6543
|
+
error: {
|
|
6544
|
+
code: "manifest_pending",
|
|
6545
|
+
message: "Photo variants are still being generated. Re-fetch the public manifest once ready flips true.",
|
|
6546
|
+
status: 202
|
|
6547
|
+
}
|
|
6548
|
+
};
|
|
6549
|
+
}
|
|
6550
|
+
};
|
|
6551
|
+
function detectKind(contentType) {
|
|
6552
|
+
if (contentType.startsWith("image/")) return "image";
|
|
6553
|
+
if (contentType.startsWith("video/")) return "video";
|
|
6554
|
+
if (contentType.startsWith("audio/")) return "audio";
|
|
6555
|
+
if (contentType.includes("pdf") || contentType.includes("text/") || contentType.includes("word") || contentType.includes("sheet") || contentType.includes("presentation")) {
|
|
6556
|
+
return "document";
|
|
6557
|
+
}
|
|
6558
|
+
return "other";
|
|
6559
|
+
}
|
|
6560
|
+
function resolveVisibility(options) {
|
|
6561
|
+
if (options?.visibility) return options.visibility;
|
|
6562
|
+
if (options?.isPublic === true) return "app_public";
|
|
6563
|
+
return "private";
|
|
6564
|
+
}
|
|
6565
|
+
function resolveFileVisibility(file, fallback = "private") {
|
|
6566
|
+
if (file.visibility) return file.visibility;
|
|
6567
|
+
if (file.is_public === true) return "app_public";
|
|
6568
|
+
return fallback;
|
|
6569
|
+
}
|
|
6570
|
+
function shouldGateOnPipeline(policy) {
|
|
6571
|
+
return policy === "safe_public" || policy === "moderated" || policy === "compliance";
|
|
6572
|
+
}
|
|
6573
|
+
function resolvePresetSpec(preset, customWidths) {
|
|
6574
|
+
if (preset === "custom") {
|
|
6575
|
+
const widths = Array.from(
|
|
6576
|
+
new Set((customWidths ?? []).filter((value) => Number.isFinite(value) && value > 0))
|
|
6577
|
+
).sort((a, b) => a - b);
|
|
6578
|
+
return { widths: widths.length > 0 ? widths : MEDIA_PRESETS.inline.widths };
|
|
6579
|
+
}
|
|
6580
|
+
return MEDIA_PRESETS[preset];
|
|
6581
|
+
}
|
|
6582
|
+
function transformOptions(width, fit) {
|
|
6583
|
+
return fit === "cover" ? { width, height: width, fit: "cover" } : { width, fit: fit ?? "contain" };
|
|
6584
|
+
}
|
|
6585
|
+
function pickDefaultWidth(widths) {
|
|
6586
|
+
if (widths.includes(1200)) return 1200;
|
|
6587
|
+
return widths[Math.floor(widths.length / 2)] ?? widths[0] ?? 0;
|
|
6588
|
+
}
|
|
6589
|
+
function fallbackFileInfo(fileId, contentType, visibility, originalUrl, cdnUrl) {
|
|
6590
|
+
return {
|
|
6591
|
+
id: fileId,
|
|
6592
|
+
filename: fileId,
|
|
6593
|
+
content_type: contentType,
|
|
6594
|
+
size_bytes: 0,
|
|
6595
|
+
is_public: visibility !== "private",
|
|
6596
|
+
visibility,
|
|
6597
|
+
cdn_url: cdnUrl ?? void 0,
|
|
6598
|
+
url: originalUrl ?? void 0,
|
|
6599
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
6600
|
+
};
|
|
6601
|
+
}
|
|
6602
|
+
function normalizeThrownError(error) {
|
|
6603
|
+
if (typeof error === "object" && error && "code" in error && "message" in error) {
|
|
6604
|
+
return error;
|
|
6605
|
+
}
|
|
6606
|
+
return {
|
|
6607
|
+
code: "upload_error",
|
|
6608
|
+
message: error instanceof Error ? error.message : "Media operation failed",
|
|
6609
|
+
status: 500
|
|
6610
|
+
};
|
|
6611
|
+
}
|
|
6612
|
+
function scanError(status) {
|
|
6613
|
+
switch (status) {
|
|
6614
|
+
case "threat":
|
|
6615
|
+
return {
|
|
6616
|
+
code: "file_threat",
|
|
6617
|
+
message: "File was flagged as malicious during scanning.",
|
|
6618
|
+
status: 409
|
|
6619
|
+
};
|
|
6620
|
+
case "quarantined":
|
|
6621
|
+
return {
|
|
6622
|
+
code: "file_quarantined",
|
|
6623
|
+
message: "File was quarantined during scanning.",
|
|
6624
|
+
status: 409
|
|
6625
|
+
};
|
|
6626
|
+
default:
|
|
6627
|
+
return {
|
|
6628
|
+
code: "upload_error",
|
|
6629
|
+
message: "File scan failed.",
|
|
6630
|
+
status: 500
|
|
6631
|
+
};
|
|
6632
|
+
}
|
|
6633
|
+
}
|
|
6634
|
+
function sleep4(ms) {
|
|
6635
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6636
|
+
}
|
|
5937
6637
|
var AudioService = class extends ServiceModule {
|
|
5938
6638
|
/**
|
|
5939
6639
|
* @param storage Required for {@link uploadViaStorage}. Wired up by the
|
|
@@ -6847,6 +7547,7 @@ var ScaleMule = class {
|
|
|
6847
7547
|
this.functions = new FunctionsService(this._client);
|
|
6848
7548
|
this.photo = new PhotoService(this._client, this.storage);
|
|
6849
7549
|
this.audio = new AudioService(this._client, this.storage);
|
|
7550
|
+
this.media = new MediaService(this._client, this.storage, this.photo, this.video, this.audio);
|
|
6850
7551
|
this.tts = new TtsService(this._client);
|
|
6851
7552
|
this.flagContent = new FlagContentService(this._client);
|
|
6852
7553
|
this.creatorMaker = new CreatorMakerService(this._client);
|
|
@@ -6997,9 +7698,9 @@ var GATEWAY_URLS2 = {
|
|
|
6997
7698
|
dev: "https://api-dev.scalemule.com",
|
|
6998
7699
|
prod: "https://api.scalemule.com"
|
|
6999
7700
|
};
|
|
7000
|
-
var SESSION_STORAGE_KEY2 =
|
|
7001
|
-
var USER_ID_STORAGE_KEY2 =
|
|
7002
|
-
var WORKSPACE_STORAGE_KEY2 =
|
|
7701
|
+
var SESSION_STORAGE_KEY2 = STORAGE_KEYS.SESSION;
|
|
7702
|
+
var USER_ID_STORAGE_KEY2 = STORAGE_KEYS.USER_ID;
|
|
7703
|
+
var WORKSPACE_STORAGE_KEY2 = STORAGE_KEYS.WORKSPACE_ID;
|
|
7003
7704
|
var RETRYABLE_STATUS_CODES3 = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
7004
7705
|
function sleep3(ms) {
|
|
7005
7706
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -7259,6 +7960,15 @@ var ScaleMuleClient2 = class {
|
|
|
7259
7960
|
this.sessionGate = null;
|
|
7260
7961
|
this.resolveSessionGate = null;
|
|
7261
7962
|
this.workspaceId = null;
|
|
7963
|
+
// Anonymous visitor ID for identity linking. See `@scalemule/sdk/anonymous-id`
|
|
7964
|
+
// for the contract. The base SDK and this client share the same canonical
|
|
7965
|
+
// localStorage key so a visitor has exactly one ID across all packages.
|
|
7966
|
+
this.anonymousId = null;
|
|
7967
|
+
// Per-client single-flight cache for the lazy mint. Concurrent first-use
|
|
7968
|
+
// callers receive the same promise — without this, two simultaneous
|
|
7969
|
+
// unauthenticated requests on a fresh visitor could mint different IDs.
|
|
7970
|
+
// The shared helper also single-flights at the storage-adapter level.
|
|
7971
|
+
this.anonymousIdPromise = null;
|
|
7262
7972
|
this.refreshPromise = null;
|
|
7263
7973
|
this.apiKey = config.apiKey;
|
|
7264
7974
|
this.applicationId = config.applicationId || null;
|
|
@@ -7395,6 +8105,10 @@ var ScaleMuleClient2 = class {
|
|
|
7395
8105
|
if (userId) this.userId = userId;
|
|
7396
8106
|
const wsId = await this.storage.getItem(WORKSPACE_STORAGE_KEY2);
|
|
7397
8107
|
if (wsId) this.workspaceId = wsId;
|
|
8108
|
+
try {
|
|
8109
|
+
await this.ensureAnonymousId();
|
|
8110
|
+
} catch {
|
|
8111
|
+
}
|
|
7398
8112
|
if (token) {
|
|
7399
8113
|
this.resolveSessionPending();
|
|
7400
8114
|
}
|
|
@@ -7402,6 +8116,32 @@ var ScaleMuleClient2 = class {
|
|
|
7402
8116
|
console.log("[ScaleMule] Initialized with session:", !!token);
|
|
7403
8117
|
}
|
|
7404
8118
|
}
|
|
8119
|
+
/**
|
|
8120
|
+
* Sync accessor for the cached anonymous ID. Returns `null` until either
|
|
8121
|
+
* `initialize()` has run or `ensureAnonymousId()` has been awaited at
|
|
8122
|
+
* least once. Use `ensureAnonymousId()` if you need a guaranteed value.
|
|
8123
|
+
*/
|
|
8124
|
+
getAnonymousId() {
|
|
8125
|
+
return this.anonymousId;
|
|
8126
|
+
}
|
|
8127
|
+
/**
|
|
8128
|
+
* Lazy-mint or cache-then-return the anonymous ID via the shared
|
|
8129
|
+
* `@scalemule/sdk` helper. Concurrent callers receive the same in-flight
|
|
8130
|
+
* promise, so a burst of simultaneous first-use requests all see the
|
|
8131
|
+
* same minted value.
|
|
8132
|
+
*/
|
|
8133
|
+
async ensureAnonymousId() {
|
|
8134
|
+
if (this.anonymousId) return this.anonymousId;
|
|
8135
|
+
if (!this.anonymousIdPromise) {
|
|
8136
|
+
this.anonymousIdPromise = ensureAnonymousId(this.storage).then((id) => {
|
|
8137
|
+
this.anonymousId = id;
|
|
8138
|
+
return id;
|
|
8139
|
+
}).finally(() => {
|
|
8140
|
+
this.anonymousIdPromise = null;
|
|
8141
|
+
});
|
|
8142
|
+
}
|
|
8143
|
+
return this.anonymousIdPromise;
|
|
8144
|
+
}
|
|
7405
8145
|
/**
|
|
7406
8146
|
* Set session after login
|
|
7407
8147
|
*/
|
|
@@ -7476,6 +8216,9 @@ var ScaleMuleClient2 = class {
|
|
|
7476
8216
|
if (this.workspaceId) {
|
|
7477
8217
|
headers.set("x-sm-workspace-id", this.workspaceId);
|
|
7478
8218
|
}
|
|
8219
|
+
if (!options?.skipAuth && !this.sessionToken && this.anonymousId) {
|
|
8220
|
+
headers.set("x-anonymous-id", this.anonymousId);
|
|
8221
|
+
}
|
|
7479
8222
|
if (!headers.has("Content-Type") && options?.body && typeof options.body === "string") {
|
|
7480
8223
|
headers.set("Content-Type", "application/json");
|
|
7481
8224
|
}
|
|
@@ -7488,6 +8231,12 @@ var ScaleMuleClient2 = class {
|
|
|
7488
8231
|
if (this.sessionGate) {
|
|
7489
8232
|
await this.sessionGate;
|
|
7490
8233
|
}
|
|
8234
|
+
if (!options.skipAuth && !this.sessionToken && !this.anonymousId) {
|
|
8235
|
+
try {
|
|
8236
|
+
await this.ensureAnonymousId();
|
|
8237
|
+
} catch {
|
|
8238
|
+
}
|
|
8239
|
+
}
|
|
7491
8240
|
const url = `${this.gatewayUrl}${path}`;
|
|
7492
8241
|
const headers = this.buildHeaders(options);
|
|
7493
8242
|
const maxRetries = options.skipRetry ? 0 : options.retries ?? 2;
|
|
@@ -8155,13 +8904,14 @@ function ScaleMuleProvider({
|
|
|
8155
8904
|
analyticsProxyUrl,
|
|
8156
8905
|
authProxyUrl,
|
|
8157
8906
|
publishableKey,
|
|
8907
|
+
apiKey,
|
|
8158
8908
|
gatewayUrl: resolvedGatewayUrl,
|
|
8159
8909
|
environment: environment || void 0,
|
|
8160
8910
|
enableAccountSwitcher,
|
|
8161
8911
|
accountSwitcherPrivacy,
|
|
8162
8912
|
bootstrapFlags
|
|
8163
8913
|
}),
|
|
8164
|
-
[client, money, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags, effectiveMediaPolicy]
|
|
8914
|
+
[client, money, baseClient, user, handleSetUser, initializing, error, analyticsProxyUrl, authProxyUrl, publishableKey, apiKey, resolvedGatewayUrl, environment, enableAccountSwitcher, accountSwitcherPrivacy, bootstrapFlags, effectiveMediaPolicy]
|
|
8165
8915
|
);
|
|
8166
8916
|
return /* @__PURE__ */ jsx(ScaleMuleContext.Provider, { value, children });
|
|
8167
8917
|
}
|
|
@@ -8222,6 +8972,19 @@ function getCookie(name) {
|
|
|
8222
8972
|
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
|
8223
8973
|
return match ? decodeURIComponent(match[1]) : void 0;
|
|
8224
8974
|
}
|
|
8975
|
+
var browserStorage = typeof window !== "undefined" && typeof window.localStorage !== "undefined" ? {
|
|
8976
|
+
getItem: (key) => window.localStorage.getItem(key),
|
|
8977
|
+
setItem: (key, value) => window.localStorage.setItem(key, value),
|
|
8978
|
+
removeItem: (key) => window.localStorage.removeItem(key)
|
|
8979
|
+
} : null;
|
|
8980
|
+
async function getProxyAnonymousId() {
|
|
8981
|
+
if (!browserStorage) return void 0;
|
|
8982
|
+
try {
|
|
8983
|
+
return await ensureAnonymousId(browserStorage);
|
|
8984
|
+
} catch {
|
|
8985
|
+
return void 0;
|
|
8986
|
+
}
|
|
8987
|
+
}
|
|
8225
8988
|
async function proxyFetch(proxyUrl, path, options = {}) {
|
|
8226
8989
|
const method = options.method || "POST";
|
|
8227
8990
|
const headers = {};
|
|
@@ -8234,6 +8997,10 @@ async function proxyFetch(proxyUrl, path, options = {}) {
|
|
|
8234
8997
|
headers["x-csrf-token"] = csrfToken;
|
|
8235
8998
|
}
|
|
8236
8999
|
}
|
|
9000
|
+
const anonymousId = await getProxyAnonymousId();
|
|
9001
|
+
if (anonymousId) {
|
|
9002
|
+
headers["x-anonymous-id"] = anonymousId;
|
|
9003
|
+
}
|
|
8237
9004
|
const response = await fetch(`${proxyUrl}/${path}`, {
|
|
8238
9005
|
method,
|
|
8239
9006
|
headers,
|
|
@@ -9705,6 +10472,442 @@ function useTtsJob(jobId, options) {
|
|
|
9705
10472
|
}, [jobId, options?.enabled, options?.pollIntervalMs, refresh]);
|
|
9706
10473
|
return { job, loading, error, refresh };
|
|
9707
10474
|
}
|
|
10475
|
+
var SPEEDS = [1, 1.25, 1.5, 2];
|
|
10476
|
+
var EXPIRY_REFRESH_WINDOW_MS = 3e4;
|
|
10477
|
+
function formatTime(seconds) {
|
|
10478
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return "0:00";
|
|
10479
|
+
const wholeSeconds = Math.floor(seconds);
|
|
10480
|
+
const minutes = Math.floor(wholeSeconds / 60);
|
|
10481
|
+
const remainder = wholeSeconds % 60;
|
|
10482
|
+
return `${minutes}:${remainder.toString().padStart(2, "0")}`;
|
|
10483
|
+
}
|
|
10484
|
+
function getNextSpeed(currentSpeed) {
|
|
10485
|
+
const currentIndex = SPEEDS.findIndex((speed) => speed === currentSpeed);
|
|
10486
|
+
if (currentIndex === -1) return SPEEDS[0];
|
|
10487
|
+
return SPEEDS[(currentIndex + 1) % SPEEDS.length];
|
|
10488
|
+
}
|
|
10489
|
+
var styles = {
|
|
10490
|
+
root: {
|
|
10491
|
+
borderRadius: "1.75rem",
|
|
10492
|
+
border: "1px solid rgba(226, 232, 240, 0.8)",
|
|
10493
|
+
background: "linear-gradient(180deg, #f8fbff 0%, #edf4ff 100%)",
|
|
10494
|
+
padding: "1rem",
|
|
10495
|
+
color: "#0f172b",
|
|
10496
|
+
boxShadow: "0 22px 70px rgba(4, 12, 24, 0.28)"
|
|
10497
|
+
},
|
|
10498
|
+
layout: {
|
|
10499
|
+
display: "flex",
|
|
10500
|
+
flexWrap: "wrap",
|
|
10501
|
+
alignItems: "center",
|
|
10502
|
+
gap: "1rem"
|
|
10503
|
+
},
|
|
10504
|
+
playButton: {
|
|
10505
|
+
display: "flex",
|
|
10506
|
+
height: "4.5rem",
|
|
10507
|
+
width: "4.5rem",
|
|
10508
|
+
flexShrink: 0,
|
|
10509
|
+
alignItems: "center",
|
|
10510
|
+
justifyContent: "center",
|
|
10511
|
+
borderRadius: "9999px",
|
|
10512
|
+
border: "none",
|
|
10513
|
+
backgroundColor: "#020618",
|
|
10514
|
+
color: "#ffffff",
|
|
10515
|
+
boxShadow: "0 10px 28px rgba(15, 23, 42, 0.25)",
|
|
10516
|
+
cursor: "pointer"
|
|
10517
|
+
},
|
|
10518
|
+
playIcon: { display: "block", width: "1.75rem", height: "1.75rem", fill: "currentColor" },
|
|
10519
|
+
playIconOffset: { marginLeft: "0.25rem" },
|
|
10520
|
+
content: { minWidth: "16rem", flex: "1 1 18rem" },
|
|
10521
|
+
metaRow: {
|
|
10522
|
+
display: "flex",
|
|
10523
|
+
alignItems: "center",
|
|
10524
|
+
justifyContent: "space-between",
|
|
10525
|
+
gap: "0.75rem",
|
|
10526
|
+
fontSize: "0.8rem",
|
|
10527
|
+
fontWeight: 500,
|
|
10528
|
+
color: "#62748e"
|
|
10529
|
+
},
|
|
10530
|
+
metaRight: { display: "flex", alignItems: "center", gap: "0.75rem" },
|
|
10531
|
+
badge: {
|
|
10532
|
+
borderRadius: "9999px",
|
|
10533
|
+
backgroundColor: "#dbeafe",
|
|
10534
|
+
padding: "0.25rem 0.75rem",
|
|
10535
|
+
fontSize: "0.65rem",
|
|
10536
|
+
fontWeight: 600,
|
|
10537
|
+
letterSpacing: "0.24em",
|
|
10538
|
+
textTransform: "uppercase",
|
|
10539
|
+
color: "#1447e6"
|
|
10540
|
+
},
|
|
10541
|
+
scrubber: {
|
|
10542
|
+
position: "relative",
|
|
10543
|
+
marginTop: "1rem",
|
|
10544
|
+
height: "3rem",
|
|
10545
|
+
cursor: "pointer",
|
|
10546
|
+
overflow: "hidden",
|
|
10547
|
+
borderRadius: "1rem",
|
|
10548
|
+
backgroundColor: "rgba(226, 232, 240, 0.9)",
|
|
10549
|
+
paddingInline: "0.25rem"
|
|
10550
|
+
},
|
|
10551
|
+
waveform: {
|
|
10552
|
+
pointerEvents: "none",
|
|
10553
|
+
position: "absolute",
|
|
10554
|
+
inset: 0,
|
|
10555
|
+
height: "100%",
|
|
10556
|
+
width: "100%"
|
|
10557
|
+
},
|
|
10558
|
+
fallbackTrack: { position: "absolute", inset: 0, backgroundColor: "rgba(226, 232, 240, 0.9)" },
|
|
10559
|
+
fallbackProgress: {
|
|
10560
|
+
position: "absolute",
|
|
10561
|
+
insetBlock: 0,
|
|
10562
|
+
left: 0,
|
|
10563
|
+
backgroundColor: "#155dfc"
|
|
10564
|
+
},
|
|
10565
|
+
chipsRow: {
|
|
10566
|
+
marginTop: "1rem",
|
|
10567
|
+
display: "flex",
|
|
10568
|
+
flexWrap: "wrap",
|
|
10569
|
+
alignItems: "center",
|
|
10570
|
+
gap: "0.5rem",
|
|
10571
|
+
fontSize: "0.72rem",
|
|
10572
|
+
fontWeight: 500,
|
|
10573
|
+
color: "#62748e"
|
|
10574
|
+
},
|
|
10575
|
+
chipStrong: {
|
|
10576
|
+
borderRadius: "9999px",
|
|
10577
|
+
border: "1px solid #e2e8f0",
|
|
10578
|
+
backgroundColor: "rgba(255, 255, 255, 0.85)",
|
|
10579
|
+
padding: "0.25rem 0.75rem",
|
|
10580
|
+
color: "#334155"
|
|
10581
|
+
},
|
|
10582
|
+
chipMuted: {
|
|
10583
|
+
borderRadius: "9999px",
|
|
10584
|
+
border: "1px solid #e2e8f0",
|
|
10585
|
+
backgroundColor: "rgba(255, 255, 255, 0.65)",
|
|
10586
|
+
padding: "0.25rem 0.75rem"
|
|
10587
|
+
},
|
|
10588
|
+
controls: {
|
|
10589
|
+
display: "flex",
|
|
10590
|
+
flexShrink: 0,
|
|
10591
|
+
alignItems: "center",
|
|
10592
|
+
gap: "0.5rem",
|
|
10593
|
+
alignSelf: "flex-start"
|
|
10594
|
+
},
|
|
10595
|
+
secondaryButton: {
|
|
10596
|
+
borderRadius: "9999px",
|
|
10597
|
+
border: "1px solid #cad5e2",
|
|
10598
|
+
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
|
10599
|
+
padding: "0.5rem 1rem",
|
|
10600
|
+
fontSize: "1rem",
|
|
10601
|
+
fontWeight: 600,
|
|
10602
|
+
color: "#334155",
|
|
10603
|
+
boxShadow: "0 1px 3px rgba(15, 23, 42, 0.08)",
|
|
10604
|
+
cursor: "pointer"
|
|
10605
|
+
},
|
|
10606
|
+
refreshButton: {
|
|
10607
|
+
borderRadius: "9999px",
|
|
10608
|
+
border: "1px solid #cad5e2",
|
|
10609
|
+
backgroundColor: "rgba(255, 255, 255, 0.75)",
|
|
10610
|
+
padding: "0.5rem 1rem",
|
|
10611
|
+
fontSize: "0.875rem",
|
|
10612
|
+
fontWeight: 600,
|
|
10613
|
+
color: "#334155",
|
|
10614
|
+
cursor: "pointer"
|
|
10615
|
+
},
|
|
10616
|
+
disabled: { cursor: "not-allowed", opacity: 0.6 }
|
|
10617
|
+
};
|
|
10618
|
+
function NarrationPlayer({
|
|
10619
|
+
audio,
|
|
10620
|
+
className,
|
|
10621
|
+
providerLabel = "ScaleMule",
|
|
10622
|
+
narrationLabel = "AI-Narrated",
|
|
10623
|
+
refreshing = false,
|
|
10624
|
+
onRefresh,
|
|
10625
|
+
showRefreshButton = false,
|
|
10626
|
+
onPlaybackError
|
|
10627
|
+
}) {
|
|
10628
|
+
const audioRef = useRef(null);
|
|
10629
|
+
const scrubRef = useRef(null);
|
|
10630
|
+
const pendingResumeRef = useRef(false);
|
|
10631
|
+
const pendingResumeTimeRef = useRef(null);
|
|
10632
|
+
const recoveredUrlRef = useRef(null);
|
|
10633
|
+
const [playing, setPlaying] = useState(false);
|
|
10634
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
10635
|
+
const [duration, setDuration] = useState(
|
|
10636
|
+
audio.duration_ms ? audio.duration_ms / 1e3 : 0
|
|
10637
|
+
);
|
|
10638
|
+
const [speed, setSpeed] = useState(1.5);
|
|
10639
|
+
const peaks = useMemo(() => {
|
|
10640
|
+
if (!Array.isArray(audio.waveform_peaks)) return [];
|
|
10641
|
+
const sampled = audio.waveform_peaks.filter((_, index) => index % 3 === 0).slice(0, 220);
|
|
10642
|
+
return sampled.length > 0 ? sampled : audio.waveform_peaks.slice(0, 220);
|
|
10643
|
+
}, [audio.waveform_peaks]);
|
|
10644
|
+
useEffect(() => {
|
|
10645
|
+
const element = audioRef.current;
|
|
10646
|
+
if (!element) return;
|
|
10647
|
+
recoveredUrlRef.current = null;
|
|
10648
|
+
const resumeTime = pendingResumeTimeRef.current;
|
|
10649
|
+
setCurrentTime(resumeTime ?? 0);
|
|
10650
|
+
setPlaying(false);
|
|
10651
|
+
element.pause();
|
|
10652
|
+
try {
|
|
10653
|
+
element.currentTime = resumeTime ?? 0;
|
|
10654
|
+
} catch {
|
|
10655
|
+
element.currentTime = 0;
|
|
10656
|
+
}
|
|
10657
|
+
if (pendingResumeRef.current) {
|
|
10658
|
+
pendingResumeRef.current = false;
|
|
10659
|
+
pendingResumeTimeRef.current = null;
|
|
10660
|
+
element.play().catch(() => {
|
|
10661
|
+
setPlaying(false);
|
|
10662
|
+
onPlaybackError?.();
|
|
10663
|
+
});
|
|
10664
|
+
return;
|
|
10665
|
+
}
|
|
10666
|
+
pendingResumeTimeRef.current = null;
|
|
10667
|
+
}, [audio.url]);
|
|
10668
|
+
useEffect(() => {
|
|
10669
|
+
const element = audioRef.current;
|
|
10670
|
+
if (!element) return;
|
|
10671
|
+
element.playbackRate = speed;
|
|
10672
|
+
element.preservesPitch = true;
|
|
10673
|
+
}, [speed]);
|
|
10674
|
+
function isUrlExpiringSoon() {
|
|
10675
|
+
if (!audio.expires_at) return false;
|
|
10676
|
+
const expiresAtMs = Date.parse(audio.expires_at);
|
|
10677
|
+
if (!Number.isFinite(expiresAtMs)) return false;
|
|
10678
|
+
return expiresAtMs - Date.now() <= EXPIRY_REFRESH_WINDOW_MS;
|
|
10679
|
+
}
|
|
10680
|
+
function seekToTime(nextTime) {
|
|
10681
|
+
const element = audioRef.current;
|
|
10682
|
+
if (!element || !duration) return;
|
|
10683
|
+
const clampedTime = Math.max(0, Math.min(duration, nextTime));
|
|
10684
|
+
element.currentTime = clampedTime;
|
|
10685
|
+
setCurrentTime(clampedTime);
|
|
10686
|
+
}
|
|
10687
|
+
async function refreshAndResume() {
|
|
10688
|
+
if (!onRefresh || refreshing) return false;
|
|
10689
|
+
if (recoveredUrlRef.current === (audio.url ?? null)) return false;
|
|
10690
|
+
recoveredUrlRef.current = audio.url ?? null;
|
|
10691
|
+
pendingResumeRef.current = true;
|
|
10692
|
+
pendingResumeTimeRef.current = currentTime;
|
|
10693
|
+
try {
|
|
10694
|
+
await Promise.resolve(onRefresh());
|
|
10695
|
+
return true;
|
|
10696
|
+
} catch {
|
|
10697
|
+
recoveredUrlRef.current = null;
|
|
10698
|
+
pendingResumeRef.current = false;
|
|
10699
|
+
pendingResumeTimeRef.current = null;
|
|
10700
|
+
onPlaybackError?.();
|
|
10701
|
+
return false;
|
|
10702
|
+
}
|
|
10703
|
+
}
|
|
10704
|
+
function togglePlayback() {
|
|
10705
|
+
const element = audioRef.current;
|
|
10706
|
+
if (!element) return;
|
|
10707
|
+
if (element.paused) {
|
|
10708
|
+
if (refreshing) {
|
|
10709
|
+
pendingResumeRef.current = true;
|
|
10710
|
+
pendingResumeTimeRef.current = currentTime;
|
|
10711
|
+
return;
|
|
10712
|
+
}
|
|
10713
|
+
if (onRefresh && isUrlExpiringSoon()) {
|
|
10714
|
+
void refreshAndResume();
|
|
10715
|
+
return;
|
|
10716
|
+
}
|
|
10717
|
+
element.play().catch(() => {
|
|
10718
|
+
setPlaying(false);
|
|
10719
|
+
onPlaybackError?.();
|
|
10720
|
+
});
|
|
10721
|
+
return;
|
|
10722
|
+
}
|
|
10723
|
+
element.pause();
|
|
10724
|
+
}
|
|
10725
|
+
function seekTo(clientX) {
|
|
10726
|
+
const scrubber = scrubRef.current;
|
|
10727
|
+
if (!scrubber || !duration) return;
|
|
10728
|
+
const bounds = scrubber.getBoundingClientRect();
|
|
10729
|
+
const ratio = Math.max(0, Math.min(1, (clientX - bounds.left) / bounds.width));
|
|
10730
|
+
seekToTime(ratio * duration);
|
|
10731
|
+
}
|
|
10732
|
+
function handleScrubberKeyDown(event) {
|
|
10733
|
+
if (!duration) return;
|
|
10734
|
+
const step = Math.max(5, duration / 20);
|
|
10735
|
+
switch (event.key) {
|
|
10736
|
+
case "ArrowLeft":
|
|
10737
|
+
case "ArrowDown":
|
|
10738
|
+
event.preventDefault();
|
|
10739
|
+
seekToTime(currentTime - step);
|
|
10740
|
+
break;
|
|
10741
|
+
case "ArrowRight":
|
|
10742
|
+
case "ArrowUp":
|
|
10743
|
+
event.preventDefault();
|
|
10744
|
+
seekToTime(currentTime + step);
|
|
10745
|
+
break;
|
|
10746
|
+
case "Home":
|
|
10747
|
+
event.preventDefault();
|
|
10748
|
+
seekToTime(0);
|
|
10749
|
+
break;
|
|
10750
|
+
case "End":
|
|
10751
|
+
event.preventDefault();
|
|
10752
|
+
seekToTime(duration);
|
|
10753
|
+
break;
|
|
10754
|
+
}
|
|
10755
|
+
}
|
|
10756
|
+
function handleManualRefresh() {
|
|
10757
|
+
if (!onRefresh || refreshing) return;
|
|
10758
|
+
Promise.resolve(onRefresh()).catch(() => {
|
|
10759
|
+
onPlaybackError?.();
|
|
10760
|
+
});
|
|
10761
|
+
}
|
|
10762
|
+
const progress = duration > 0 ? Math.min(1, currentTime / duration) : 0;
|
|
10763
|
+
const expiresLabel = audio.expires_at ? new Date(audio.expires_at).toLocaleTimeString([], {
|
|
10764
|
+
hour: "numeric",
|
|
10765
|
+
minute: "2-digit"
|
|
10766
|
+
}) : null;
|
|
10767
|
+
if (!audio.url) return null;
|
|
10768
|
+
return /* @__PURE__ */ jsxs("div", { className, style: styles.root, children: [
|
|
10769
|
+
/* @__PURE__ */ jsx(
|
|
10770
|
+
"audio",
|
|
10771
|
+
{
|
|
10772
|
+
ref: audioRef,
|
|
10773
|
+
src: audio.url ?? void 0,
|
|
10774
|
+
preload: "metadata",
|
|
10775
|
+
onLoadedMetadata: (event) => {
|
|
10776
|
+
setDuration(event.currentTarget.duration || 0);
|
|
10777
|
+
},
|
|
10778
|
+
onTimeUpdate: (event) => {
|
|
10779
|
+
setCurrentTime(event.currentTarget.currentTime);
|
|
10780
|
+
},
|
|
10781
|
+
onPlay: () => setPlaying(true),
|
|
10782
|
+
onPause: () => setPlaying(false),
|
|
10783
|
+
onEnded: () => setPlaying(false),
|
|
10784
|
+
onError: () => {
|
|
10785
|
+
if (onRefresh) {
|
|
10786
|
+
void refreshAndResume().then((didRefresh) => {
|
|
10787
|
+
if (!didRefresh) onPlaybackError?.();
|
|
10788
|
+
});
|
|
10789
|
+
return;
|
|
10790
|
+
}
|
|
10791
|
+
onPlaybackError?.();
|
|
10792
|
+
}
|
|
10793
|
+
}
|
|
10794
|
+
),
|
|
10795
|
+
/* @__PURE__ */ jsxs("div", { style: styles.layout, children: [
|
|
10796
|
+
/* @__PURE__ */ jsx(
|
|
10797
|
+
"button",
|
|
10798
|
+
{
|
|
10799
|
+
type: "button",
|
|
10800
|
+
onClick: togglePlayback,
|
|
10801
|
+
"aria-label": playing ? "Pause narration" : "Play narration",
|
|
10802
|
+
style: styles.playButton,
|
|
10803
|
+
children: playing ? /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", style: styles.playIcon, "aria-hidden": "true", children: [
|
|
10804
|
+
/* @__PURE__ */ jsx("rect", { x: "6", y: "5", width: "4", height: "14", rx: "1.2" }),
|
|
10805
|
+
/* @__PURE__ */ jsx("rect", { x: "14", y: "5", width: "4", height: "14", rx: "1.2" })
|
|
10806
|
+
] }) : /* @__PURE__ */ jsx(
|
|
10807
|
+
"svg",
|
|
10808
|
+
{
|
|
10809
|
+
viewBox: "0 0 24 24",
|
|
10810
|
+
style: { ...styles.playIcon, ...styles.playIconOffset },
|
|
10811
|
+
"aria-hidden": "true",
|
|
10812
|
+
children: /* @__PURE__ */ jsx("path", { d: "M7 5.2c0-1.02 1.12-1.65 2-.98l10.2 7.78a1.24 1.24 0 0 1 0 1.98L9 21.76c-.88.67-2-.04-2-1.08V5.2Z" })
|
|
10813
|
+
}
|
|
10814
|
+
)
|
|
10815
|
+
}
|
|
10816
|
+
),
|
|
10817
|
+
/* @__PURE__ */ jsxs("div", { style: styles.content, children: [
|
|
10818
|
+
/* @__PURE__ */ jsxs("div", { style: styles.metaRow, children: [
|
|
10819
|
+
/* @__PURE__ */ jsx("span", { children: formatTime(currentTime / speed) }),
|
|
10820
|
+
/* @__PURE__ */ jsxs("div", { style: styles.metaRight, children: [
|
|
10821
|
+
/* @__PURE__ */ jsx("span", { style: styles.badge, children: narrationLabel }),
|
|
10822
|
+
/* @__PURE__ */ jsx("span", { children: formatTime(duration / speed) })
|
|
10823
|
+
] })
|
|
10824
|
+
] }),
|
|
10825
|
+
/* @__PURE__ */ jsx(
|
|
10826
|
+
"div",
|
|
10827
|
+
{
|
|
10828
|
+
ref: scrubRef,
|
|
10829
|
+
role: "slider",
|
|
10830
|
+
"aria-label": "Narration progress",
|
|
10831
|
+
"aria-valuemin": 0,
|
|
10832
|
+
"aria-valuemax": Math.max(1, Math.floor(duration)),
|
|
10833
|
+
"aria-valuenow": Math.floor(currentTime),
|
|
10834
|
+
"aria-valuetext": `${formatTime(currentTime / speed)} of ${formatTime(duration / speed)}`,
|
|
10835
|
+
tabIndex: 0,
|
|
10836
|
+
onClick: (event) => seekTo(event.clientX),
|
|
10837
|
+
onKeyDown: handleScrubberKeyDown,
|
|
10838
|
+
style: styles.scrubber,
|
|
10839
|
+
children: peaks.length > 0 ? /* @__PURE__ */ jsx(
|
|
10840
|
+
"svg",
|
|
10841
|
+
{
|
|
10842
|
+
viewBox: `0 0 ${peaks.length} 40`,
|
|
10843
|
+
preserveAspectRatio: "none",
|
|
10844
|
+
style: styles.waveform,
|
|
10845
|
+
children: peaks.map((peak, index) => {
|
|
10846
|
+
const height = Math.max(2, Math.round(Math.abs(peak) * 34));
|
|
10847
|
+
const y = (40 - height) / 2;
|
|
10848
|
+
const filled = index / peaks.length < progress;
|
|
10849
|
+
return /* @__PURE__ */ jsx(
|
|
10850
|
+
"rect",
|
|
10851
|
+
{
|
|
10852
|
+
x: index,
|
|
10853
|
+
y,
|
|
10854
|
+
width: 0.82,
|
|
10855
|
+
height,
|
|
10856
|
+
rx: 0.4,
|
|
10857
|
+
fill: filled ? "#155dfc" : "#90c5ff"
|
|
10858
|
+
},
|
|
10859
|
+
`${audio.id}-${index}`
|
|
10860
|
+
);
|
|
10861
|
+
})
|
|
10862
|
+
}
|
|
10863
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
10864
|
+
/* @__PURE__ */ jsx("div", { style: styles.fallbackTrack }),
|
|
10865
|
+
/* @__PURE__ */ jsx("div", { style: { ...styles.fallbackProgress, width: `${progress * 100}%` } })
|
|
10866
|
+
] })
|
|
10867
|
+
}
|
|
10868
|
+
),
|
|
10869
|
+
/* @__PURE__ */ jsxs("div", { style: styles.chipsRow, children: [
|
|
10870
|
+
/* @__PURE__ */ jsxs("span", { style: styles.chipStrong, children: [
|
|
10871
|
+
"Provider: ",
|
|
10872
|
+
providerLabel
|
|
10873
|
+
] }),
|
|
10874
|
+
duration > 0 && /* @__PURE__ */ jsxs("span", { style: styles.chipMuted, children: [
|
|
10875
|
+
"Length ",
|
|
10876
|
+
formatTime(duration / speed)
|
|
10877
|
+
] }),
|
|
10878
|
+
expiresLabel && /* @__PURE__ */ jsxs("span", { style: styles.chipMuted, children: [
|
|
10879
|
+
"Expires ",
|
|
10880
|
+
expiresLabel
|
|
10881
|
+
] })
|
|
10882
|
+
] })
|
|
10883
|
+
] }),
|
|
10884
|
+
/* @__PURE__ */ jsxs("div", { style: styles.controls, children: [
|
|
10885
|
+
/* @__PURE__ */ jsxs(
|
|
10886
|
+
"button",
|
|
10887
|
+
{
|
|
10888
|
+
type: "button",
|
|
10889
|
+
onClick: () => setSpeed((currentSpeed) => getNextSpeed(currentSpeed)),
|
|
10890
|
+
style: styles.secondaryButton,
|
|
10891
|
+
children: [
|
|
10892
|
+
speed,
|
|
10893
|
+
"\xD7"
|
|
10894
|
+
]
|
|
10895
|
+
}
|
|
10896
|
+
),
|
|
10897
|
+
onRefresh && showRefreshButton && /* @__PURE__ */ jsx(
|
|
10898
|
+
"button",
|
|
10899
|
+
{
|
|
10900
|
+
type: "button",
|
|
10901
|
+
onClick: handleManualRefresh,
|
|
10902
|
+
disabled: refreshing,
|
|
10903
|
+
style: refreshing ? { ...styles.refreshButton, ...styles.disabled } : styles.refreshButton,
|
|
10904
|
+
children: refreshing ? "Refreshing\u2026" : "Refresh"
|
|
10905
|
+
}
|
|
10906
|
+
)
|
|
10907
|
+
] })
|
|
10908
|
+
] })
|
|
10909
|
+
] });
|
|
10910
|
+
}
|
|
9708
10911
|
function useMedia() {
|
|
9709
10912
|
const { media: rawMedia, mediaPolicy: providerDefaultPolicy } = useScaleMule();
|
|
9710
10913
|
const media = rawMedia;
|
|
@@ -10284,16 +11487,14 @@ function setStorageItem(storage, key, value) {
|
|
|
10284
11487
|
} catch {
|
|
10285
11488
|
}
|
|
10286
11489
|
}
|
|
10287
|
-
function
|
|
11490
|
+
function getOrCreateSessionId(sessionStorageKey) {
|
|
10288
11491
|
if (typeof window === "undefined") {
|
|
10289
11492
|
return {
|
|
10290
11493
|
sessionId: null,
|
|
10291
|
-
anonymousId: null,
|
|
10292
11494
|
sessionStart: Date.now()
|
|
10293
11495
|
};
|
|
10294
11496
|
}
|
|
10295
11497
|
const storage = typeof sessionStorage !== "undefined" ? sessionStorage : void 0;
|
|
10296
|
-
const localStorage_ = typeof localStorage !== "undefined" ? localStorage : void 0;
|
|
10297
11498
|
let sessionId = getStorageItem(storage, sessionStorageKey);
|
|
10298
11499
|
let sessionStartStr = getStorageItem(storage, SESSION_START_KEY);
|
|
10299
11500
|
let sessionStart;
|
|
@@ -10305,12 +11506,7 @@ function getOrCreateIds(sessionStorageKey, anonymousStorageKey) {
|
|
|
10305
11506
|
} else {
|
|
10306
11507
|
sessionStart = parseInt(sessionStartStr, 10);
|
|
10307
11508
|
}
|
|
10308
|
-
|
|
10309
|
-
if (!anonymousId) {
|
|
10310
|
-
anonymousId = generateUUID();
|
|
10311
|
-
setStorageItem(localStorage_, anonymousStorageKey, anonymousId);
|
|
10312
|
-
}
|
|
10313
|
-
return { sessionId, anonymousId, sessionStart };
|
|
11509
|
+
return { sessionId, sessionStart };
|
|
10314
11510
|
}
|
|
10315
11511
|
function parseUtmParams() {
|
|
10316
11512
|
if (typeof window === "undefined") return null;
|
|
@@ -10421,12 +11617,27 @@ function useAnalytics(options = {}) {
|
|
|
10421
11617
|
autoCapturUtmParams,
|
|
10422
11618
|
autoGenerateSessionId = true,
|
|
10423
11619
|
sessionStorageKey = "sm_session_id",
|
|
10424
|
-
anonymousStorageKey
|
|
11620
|
+
// anonymousStorageKey is intentionally ignored — see UseAnalyticsOptions.
|
|
11621
|
+
// Anonymous ID now lives on the ScaleMule client under the canonical
|
|
11622
|
+
// `scalemule_anonymous_id` key (PR consolidating the contract). Reading
|
|
11623
|
+
// an override here would re-fragment a visitor's identity between
|
|
11624
|
+
// analytics events and auth request headers.
|
|
10425
11625
|
useV2 = true,
|
|
10426
11626
|
eventDedupMs = DEFAULT_EVENT_DEDUP_MS
|
|
10427
11627
|
} = options;
|
|
10428
11628
|
const shouldAutoCaptureUtmParams = autoCaptureUtmParams ?? autoCapturUtmParams ?? true;
|
|
10429
|
-
const { client, user, analyticsProxyUrl, publishableKey, gatewayUrl } = useScaleMule();
|
|
11629
|
+
const { client, user, analyticsProxyUrl, publishableKey, apiKey, gatewayUrl } = useScaleMule();
|
|
11630
|
+
const proxyModeMisconfigWarnedRef = useRef(false);
|
|
11631
|
+
useEffect(() => {
|
|
11632
|
+
if (process.env.NODE_ENV === "production") return;
|
|
11633
|
+
if (proxyModeMisconfigWarnedRef.current) return;
|
|
11634
|
+
if (apiKey === "proxy-mode" && !analyticsProxyUrl && !publishableKey) {
|
|
11635
|
+
proxyModeMisconfigWarnedRef.current = true;
|
|
11636
|
+
console.warn(
|
|
11637
|
+
'[ScaleMule] useAnalytics: apiKey="proxy-mode" but neither `analyticsProxyUrl` nor `publishableKey` is configured. Every trackEvent / trackPageView will silently 401 against the gateway. Add an analytics proxy route (createAnalyticsRoutes({ simpleProxy: true })) and pass `analyticsProxyUrl` to <ScaleMuleProvider>. See @scalemule/nextjs README \u2192 Proxy Mode.'
|
|
11638
|
+
);
|
|
11639
|
+
}
|
|
11640
|
+
}, [apiKey, analyticsProxyUrl, publishableKey]);
|
|
10430
11641
|
const [loading, setLoading] = useState(false);
|
|
10431
11642
|
const [error, setError] = useState(null);
|
|
10432
11643
|
const [utmParams, setUtmParams] = useState(null);
|
|
@@ -10447,14 +11658,13 @@ function useAnalytics(options = {}) {
|
|
|
10447
11658
|
idsReadyRef.current = true;
|
|
10448
11659
|
return;
|
|
10449
11660
|
}
|
|
10450
|
-
const ids =
|
|
11661
|
+
const ids = getOrCreateSessionId(sessionStorageKey);
|
|
10451
11662
|
sessionIdRef.current = ids.sessionId;
|
|
10452
|
-
anonymousIdRef.current = ids.anonymousId;
|
|
10453
11663
|
sessionStartRef.current = ids.sessionStart;
|
|
10454
|
-
idsReadyRef.current = true;
|
|
10455
11664
|
setSessionId(ids.sessionId);
|
|
10456
|
-
|
|
10457
|
-
|
|
11665
|
+
let cancelled = false;
|
|
11666
|
+
const flushQueuedEvents = () => {
|
|
11667
|
+
if (eventQueue.current.length === 0) return;
|
|
10458
11668
|
const queue = eventQueue.current;
|
|
10459
11669
|
eventQueue.current = [];
|
|
10460
11670
|
setTimeout(() => {
|
|
@@ -10462,8 +11672,22 @@ function useAnalytics(options = {}) {
|
|
|
10462
11672
|
sendEventRef.current?.(event);
|
|
10463
11673
|
}
|
|
10464
11674
|
}, 0);
|
|
10465
|
-
}
|
|
10466
|
-
|
|
11675
|
+
};
|
|
11676
|
+
client.ensureAnonymousId().then((anonId) => {
|
|
11677
|
+
if (cancelled) return;
|
|
11678
|
+
anonymousIdRef.current = anonId;
|
|
11679
|
+
setAnonymousId(anonId);
|
|
11680
|
+
idsReadyRef.current = true;
|
|
11681
|
+
flushQueuedEvents();
|
|
11682
|
+
}).catch(() => {
|
|
11683
|
+
if (cancelled) return;
|
|
11684
|
+
idsReadyRef.current = true;
|
|
11685
|
+
flushQueuedEvents();
|
|
11686
|
+
});
|
|
11687
|
+
return () => {
|
|
11688
|
+
cancelled = true;
|
|
11689
|
+
};
|
|
11690
|
+
}, [autoGenerateSessionId, sessionStorageKey, client]);
|
|
10467
11691
|
useEffect(() => {
|
|
10468
11692
|
if (typeof window === "undefined") return;
|
|
10469
11693
|
if (shouldAutoCaptureUtmParams) {
|
|
@@ -11194,7 +12418,7 @@ function useVote({
|
|
|
11194
12418
|
const [state, setState] = useState(initialState ?? ZERO);
|
|
11195
12419
|
const [isLoading, setIsLoading] = useState(!initialState && enabled);
|
|
11196
12420
|
const [error, setError] = useState(null);
|
|
11197
|
-
const
|
|
12421
|
+
const inFlight2 = useRef(null);
|
|
11198
12422
|
const path = `/v1/social/${encodeURIComponent(targetType)}/${encodeURIComponent(targetId)}/vote`;
|
|
11199
12423
|
const refetch = useCallback(async () => {
|
|
11200
12424
|
if (!enabled) return;
|
|
@@ -11229,10 +12453,10 @@ function useVote({
|
|
|
11229
12453
|
setError(e);
|
|
11230
12454
|
}
|
|
11231
12455
|
};
|
|
11232
|
-
const p =
|
|
11233
|
-
|
|
12456
|
+
const p = inFlight2.current ? inFlight2.current.then(send) : send();
|
|
12457
|
+
inFlight2.current = p;
|
|
11234
12458
|
await p;
|
|
11235
|
-
if (
|
|
12459
|
+
if (inFlight2.current === p) inFlight2.current = null;
|
|
11236
12460
|
},
|
|
11237
12461
|
[client, path, state]
|
|
11238
12462
|
);
|
|
@@ -12035,4 +13259,4 @@ function createSafeLogger(prefix) {
|
|
|
12035
13259
|
};
|
|
12036
13260
|
}
|
|
12037
13261
|
|
|
12038
|
-
export { FeedbackWidget, ScaleMuleApiError, ScaleMuleClient2 as ScaleMuleClient, ScaleMuleMedia, ScaleMuleProvider, VoteButton, composePhone, createClient, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useTtsJob, useUser, useVote, validateForm, validators };
|
|
13262
|
+
export { FeedbackWidget, NarrationPlayer, ScaleMuleApiError, ScaleMuleClient2 as ScaleMuleClient, ScaleMuleMedia, ScaleMuleProvider, VoteButton, composePhone, createClient, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAudio, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useTtsJob, useUser, useVote, validateForm, validators };
|