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