@sylphx/sdk 0.9.0 → 0.10.0

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.mjs CHANGED
@@ -8002,169 +8002,248 @@ async function getBillingUsage(config, options) {
8002
8002
  });
8003
8003
  }
8004
8004
 
8005
- // src/storage.ts
8006
- import { storageEndpoints } from "@sylphx/contract";
8005
+ // src/lib/retry.ts
8007
8006
  init_constants();
8008
- init_errors();
8009
- var UPLOAD_RETRY_CONFIG = {
8010
- /** Maximum number of retry attempts (AWS S3 pattern) */
8007
+ var DEFAULT_RETRY_CONFIG = {
8011
8008
  maxRetries: 5,
8012
- /** Base delay in milliseconds */
8013
8009
  baseDelayMs: BASE_RETRY_DELAY_MS,
8014
- /** Maximum delay cap in milliseconds */
8015
- maxDelayMs: MAX_RETRY_DELAY_MS,
8016
- /** Jitter type: 'full' for full jitter (AWS recommended) */
8017
- jitter: "full"
8010
+ maxDelayMs: MAX_RETRY_DELAY_MS
8018
8011
  };
8019
- function calculateBackoffDelay(attempt) {
8020
- const { baseDelayMs, maxDelayMs } = UPLOAD_RETRY_CONFIG;
8021
- const exponentialDelay = baseDelayMs * 2 ** attempt;
8022
- const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
8023
- return Math.random() * cappedDelay;
8012
+ function calculateBackoffDelay(attempt, config = DEFAULT_RETRY_CONFIG) {
8013
+ const exp = config.baseDelayMs * 2 ** attempt;
8014
+ const capped = Math.min(exp, config.maxDelayMs);
8015
+ return Math.random() * capped;
8024
8016
  }
8025
- async function sleep(ms, signal) {
8017
+ function sleep(ms, signal) {
8026
8018
  return new Promise((resolve, reject) => {
8027
8019
  if (signal?.aborted) {
8028
- reject(new DOMException("Upload aborted", "AbortError"));
8020
+ reject(toAbortError());
8029
8021
  return;
8030
8022
  }
8031
- const timeoutId = setTimeout(resolve, ms);
8023
+ const timer = setTimeout(resolve, ms);
8032
8024
  signal?.addEventListener(
8033
8025
  "abort",
8034
8026
  () => {
8035
- clearTimeout(timeoutId);
8036
- reject(new DOMException("Upload aborted", "AbortError"));
8027
+ clearTimeout(timer);
8028
+ reject(toAbortError());
8037
8029
  },
8038
8030
  { once: true }
8039
8031
  );
8040
8032
  });
8041
8033
  }
8034
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
8042
8035
  function isRetryableError2(error) {
8043
- if (error instanceof DOMException && error.name === "AbortError") {
8044
- return false;
8045
- }
8046
- if (error instanceof TypeError) {
8047
- return true;
8048
- }
8036
+ if (isAbortError(error)) return false;
8037
+ if (error instanceof TypeError) return true;
8049
8038
  if (error instanceof Error && "status" in error) {
8050
8039
  const status = error.status;
8051
- return status >= 500 || status === 429;
8040
+ return status >= 500 || RETRYABLE_STATUSES.has(status);
8052
8041
  }
8053
8042
  return false;
8054
8043
  }
8055
- async function uploadFile(config, file, options) {
8056
- const { signal } = options ?? {};
8057
- if (signal?.aborted) {
8058
- throw new DOMException("Upload aborted", "AbortError");
8044
+ function isAbortError(error) {
8045
+ if (error instanceof DOMException && error.name === "AbortError") return true;
8046
+ if (error instanceof Error && error.name === "AbortError") return true;
8047
+ return false;
8048
+ }
8049
+ function toAbortError(message2 = "Aborted") {
8050
+ if (typeof DOMException !== "undefined") {
8051
+ return new DOMException(message2, "AbortError");
8059
8052
  }
8060
- let tokenResponse = null;
8061
- let lastError = null;
8062
- for (let attempt = 0; attempt <= UPLOAD_RETRY_CONFIG.maxRetries; attempt++) {
8053
+ const err = new Error(message2);
8054
+ err.name = "AbortError";
8055
+ return err;
8056
+ }
8057
+ async function withRetry(fn, options = {}) {
8058
+ const cfg = {
8059
+ maxRetries: options.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries,
8060
+ baseDelayMs: options.baseDelayMs ?? DEFAULT_RETRY_CONFIG.baseDelayMs,
8061
+ maxDelayMs: options.maxDelayMs ?? DEFAULT_RETRY_CONFIG.maxDelayMs
8062
+ };
8063
+ let lastError;
8064
+ for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
8065
+ if (options.signal?.aborted) throw toAbortError("Aborted");
8063
8066
  try {
8064
- tokenResponse = await fetch(buildApiUrl(config, "/storage/upload"), {
8065
- method: "POST",
8066
- headers: buildHeaders(config),
8067
- body: JSON.stringify({
8068
- filename: file.name,
8069
- contentType: file.type,
8070
- size: file.size,
8071
- path: options?.path,
8072
- type: options?.type ?? "file",
8073
- userId: options?.userId
8074
- }),
8075
- signal
8076
- });
8077
- if (tokenResponse.ok) {
8078
- break;
8079
- }
8080
- if (tokenResponse.status >= 500 || tokenResponse.status === 429) {
8081
- if (attempt < UPLOAD_RETRY_CONFIG.maxRetries) {
8082
- const delay = calculateBackoffDelay(attempt);
8083
- await sleep(delay, signal);
8084
- continue;
8085
- }
8086
- }
8087
- const error = await tokenResponse.json().catch(() => ({ message: "Failed to get upload token" }));
8088
- throw new SylphxError(error.message ?? "Failed to get upload token", {
8089
- code: "BAD_REQUEST"
8090
- });
8091
- } catch (error) {
8092
- if (error instanceof DOMException && error.name === "AbortError") {
8093
- throw error;
8094
- }
8095
- lastError = error instanceof Error ? error : new Error(String(error));
8096
- if (isRetryableError2(error) && attempt < UPLOAD_RETRY_CONFIG.maxRetries) {
8097
- const delay = calculateBackoffDelay(attempt);
8098
- await sleep(delay, signal);
8099
- continue;
8100
- }
8101
- throw lastError;
8067
+ return await fn();
8068
+ } catch (err) {
8069
+ lastError = err;
8070
+ if (isAbortError(err)) throw err;
8071
+ if (attempt === cfg.maxRetries) break;
8072
+ if (!isRetryableError2(err)) throw err;
8073
+ const retryAfter = extractRetryAfter(err);
8074
+ const delay = retryAfter ?? calculateBackoffDelay(attempt, cfg);
8075
+ options.onRetry?.(attempt, delay, err);
8076
+ await sleep(delay, options.signal);
8077
+ }
8078
+ }
8079
+ throw lastError;
8080
+ }
8081
+ function extractRetryAfter(err) {
8082
+ if (err && typeof err === "object" && "retryAfter" in err) {
8083
+ const v = err.retryAfter;
8084
+ if (typeof v === "number" && v > 0) return v * 1e3;
8085
+ }
8086
+ return void 0;
8087
+ }
8088
+ function uuidv7() {
8089
+ const ms = BigInt(Date.now());
8090
+ const bytes = randomBytes(16);
8091
+ bytes[0] = Number(ms >> 40n & 0xffn);
8092
+ bytes[1] = Number(ms >> 32n & 0xffn);
8093
+ bytes[2] = Number(ms >> 24n & 0xffn);
8094
+ bytes[3] = Number(ms >> 16n & 0xffn);
8095
+ bytes[4] = Number(ms >> 8n & 0xffn);
8096
+ bytes[5] = Number(ms & 0xffn);
8097
+ bytes[6] = bytes[6] & 15 | 112;
8098
+ bytes[8] = bytes[8] & 63 | 128;
8099
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
8100
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
8101
+ }
8102
+ function randomBytes(n) {
8103
+ const out = new Uint8Array(n);
8104
+ const c = globalThis.crypto;
8105
+ if (c?.getRandomValues) {
8106
+ c.getRandomValues(out);
8107
+ return out;
8108
+ }
8109
+ for (let i = 0; i < n; i++) out[i] = Math.floor(Math.random() * 256);
8110
+ return out;
8111
+ }
8112
+
8113
+ // src/storage.ts
8114
+ var PATHS = {
8115
+ uploads: "/storage/uploads",
8116
+ upload: (id) => `/storage/uploads/${encodeURIComponent(String(id))}`,
8117
+ uploadComplete: (id) => `/storage/uploads/${encodeURIComponent(String(id))}:complete`,
8118
+ uploadPart: (id, n) => `/storage/uploads/${encodeURIComponent(String(id))}/parts/${n}`,
8119
+ files: "/storage/files",
8120
+ file: (id) => `/storage/files/${encodeURIComponent(String(id))}`,
8121
+ fileRestore: (id) => `/storage/files/${encodeURIComponent(String(id))}:restore`,
8122
+ fileSignedUrl: (id) => `/storage/files/${encodeURIComponent(String(id))}:signedUrl`,
8123
+ fileCopy: (id) => `/storage/files/${encodeURIComponent(String(id))}:copy`,
8124
+ versions: (id) => `/storage/files/${encodeURIComponent(String(id))}/versions`,
8125
+ versionRestore: (id, vid) => `/storage/files/${encodeURIComponent(String(id))}/versions/${encodeURIComponent(String(vid))}:restore`
8126
+ };
8127
+ var isBrowser = () => typeof globalThis !== "undefined" && typeof globalThis.window !== "undefined";
8128
+ var hasXhr = () => typeof globalThis.XMLHttpRequest !== "undefined";
8129
+ var hasLocalStorage = () => {
8130
+ try {
8131
+ const ls = globalThis.localStorage;
8132
+ return Boolean(ls && typeof ls.getItem === "function");
8133
+ } catch {
8134
+ return false;
8135
+ }
8136
+ };
8137
+ async function computeSha256Hex(blob) {
8138
+ const subtle = globalThis.crypto?.subtle;
8139
+ if (subtle?.digest) {
8140
+ const buf2 = await blob.arrayBuffer();
8141
+ const digest2 = await subtle.digest("SHA-256", buf2);
8142
+ return bytesToHex(new Uint8Array(digest2));
8143
+ }
8144
+ const nodeCrypto = await import("crypto");
8145
+ const hash = nodeCrypto.createHash("sha256");
8146
+ const buf = Buffer.from(await blob.arrayBuffer());
8147
+ hash.update(buf);
8148
+ return hash.digest("hex");
8149
+ }
8150
+ function bytesToHex(b) {
8151
+ let s = "";
8152
+ for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
8153
+ return s;
8154
+ }
8155
+ var RESUME_KEY_PREFIX = "sylphx_upload_";
8156
+ function resumeKey(uploadId) {
8157
+ return `${RESUME_KEY_PREFIX}${uploadId}`;
8158
+ }
8159
+ function persistResume(rec) {
8160
+ if (hasLocalStorage()) {
8161
+ try {
8162
+ localStorage.setItem(resumeKey(rec.uploadId), JSON.stringify(rec));
8163
+ } catch {
8102
8164
  }
8165
+ return;
8103
8166
  }
8104
- if (!tokenResponse?.ok) {
8105
- throw lastError ?? new SylphxError("Failed to get upload token after retries", {
8106
- code: "BAD_REQUEST"
8107
- });
8167
+ if (!isBrowser()) {
8168
+ void persistResumeNode(rec);
8108
8169
  }
8109
- const { uploadUrl, publicUrl } = await tokenResponse.json();
8110
- return executeUploadWithRetry(file, uploadUrl, publicUrl, options);
8111
8170
  }
8112
- async function executeUploadWithRetry(file, uploadUrl, publicUrl, options) {
8113
- const { signal } = options ?? {};
8114
- let lastError = null;
8115
- for (let attempt = 0; attempt <= UPLOAD_RETRY_CONFIG.maxRetries; attempt++) {
8171
+ function clearResume(uploadId) {
8172
+ if (hasLocalStorage()) {
8116
8173
  try {
8117
- return await executeUpload(file, uploadUrl, publicUrl, options);
8118
- } catch (error) {
8119
- if (error instanceof DOMException && error.name === "AbortError") {
8120
- throw error;
8121
- }
8122
- lastError = error instanceof Error ? error : new Error(String(error));
8123
- if (isRetryableError2(error) && attempt < UPLOAD_RETRY_CONFIG.maxRetries) {
8124
- const delay = calculateBackoffDelay(attempt);
8125
- await sleep(delay, signal);
8126
- continue;
8127
- }
8128
- throw lastError;
8174
+ localStorage.removeItem(resumeKey(uploadId));
8175
+ } catch {
8176
+ }
8177
+ return;
8178
+ }
8179
+ if (!isBrowser()) void clearResumeNode(uploadId);
8180
+ }
8181
+ async function persistResumeNode(rec) {
8182
+ try {
8183
+ const fs = await import("fs/promises");
8184
+ const path = await import("path");
8185
+ const os = await import("os");
8186
+ const dir = path.join(os.homedir(), ".sylphx");
8187
+ await fs.mkdir(dir, { recursive: true });
8188
+ const file = path.join(dir, "uploads.json");
8189
+ let store = {};
8190
+ try {
8191
+ const text = await fs.readFile(file, "utf8");
8192
+ store = JSON.parse(text);
8193
+ } catch {
8194
+ store = {};
8129
8195
  }
8196
+ store[rec.uploadId] = rec;
8197
+ await fs.writeFile(file, JSON.stringify(store), "utf8");
8198
+ } catch {
8199
+ }
8200
+ }
8201
+ async function clearResumeNode(uploadId) {
8202
+ try {
8203
+ const fs = await import("fs/promises");
8204
+ const path = await import("path");
8205
+ const os = await import("os");
8206
+ const file = path.join(os.homedir(), ".sylphx", "uploads.json");
8207
+ const text = await fs.readFile(file, "utf8");
8208
+ const store = JSON.parse(text);
8209
+ delete store[uploadId];
8210
+ await fs.writeFile(file, JSON.stringify(store), "utf8");
8211
+ } catch {
8130
8212
  }
8131
- throw lastError ?? new Error("Upload failed after retries");
8132
8213
  }
8133
- function executeUpload(file, uploadUrl, publicUrl, options) {
8134
- const { signal, onProgress } = options ?? {};
8214
+ function putBlob(url, body, headers, signal, onProgress) {
8215
+ if (hasXhr()) return putBlobXhr(url, body, headers, signal, onProgress);
8216
+ return putBlobFetch(url, body, headers, signal, onProgress);
8217
+ }
8218
+ function putBlobXhr(url, body, headers, signal, onProgress) {
8135
8219
  return new Promise((resolve, reject) => {
8136
8220
  const xhr = new XMLHttpRequest();
8137
8221
  const handleAbort = () => {
8138
- xhr.abort();
8139
- reject(new DOMException("Upload aborted", "AbortError"));
8222
+ try {
8223
+ xhr.abort();
8224
+ } catch {
8225
+ }
8226
+ reject(toAbortError("Upload aborted"));
8140
8227
  };
8141
8228
  if (signal?.aborted) {
8142
- reject(new DOMException("Upload aborted", "AbortError"));
8229
+ reject(toAbortError("Upload aborted"));
8143
8230
  return;
8144
8231
  }
8145
8232
  signal?.addEventListener("abort", handleAbort, { once: true });
8146
- xhr.upload.addEventListener("progress", (event) => {
8147
- if (event.lengthComputable && onProgress) {
8148
- onProgress({
8149
- loaded: event.loaded,
8150
- total: event.total,
8151
- progress: Math.round(event.loaded / event.total * 100)
8152
- });
8153
- }
8154
- });
8233
+ if (onProgress) {
8234
+ xhr.upload.addEventListener("progress", (e) => {
8235
+ if (e.lengthComputable) onProgress(e.loaded);
8236
+ });
8237
+ }
8155
8238
  xhr.addEventListener("load", () => {
8156
8239
  signal?.removeEventListener("abort", handleAbort);
8157
8240
  if (xhr.status >= 200 && xhr.status < 300) {
8158
- resolve({
8159
- url: publicUrl,
8160
- pathname: options?.path ? `${options.path}/${file.name}` : file.name,
8161
- contentType: file.type,
8162
- size: file.size
8163
- });
8241
+ const etag = resolveXhrEtag(xhr);
8242
+ resolve({ etag: stripQuotes(etag), status: xhr.status });
8164
8243
  } else {
8165
- const error = new Error(`Upload failed with status ${xhr.status}`);
8166
- error.status = xhr.status;
8167
- reject(error);
8244
+ const err = new Error(`PUT failed with status ${xhr.status}`);
8245
+ err.status = xhr.status;
8246
+ reject(err);
8168
8247
  }
8169
8248
  });
8170
8249
  xhr.addEventListener("error", () => {
@@ -8173,93 +8252,291 @@ function executeUpload(file, uploadUrl, publicUrl, options) {
8173
8252
  });
8174
8253
  xhr.addEventListener("abort", () => {
8175
8254
  signal?.removeEventListener("abort", handleAbort);
8176
- reject(new DOMException("Upload aborted", "AbortError"));
8255
+ reject(toAbortError("Upload aborted"));
8177
8256
  });
8178
- xhr.open("PUT", uploadUrl);
8179
- xhr.setRequestHeader("Content-Type", file.type);
8180
- xhr.send(file);
8181
- });
8182
- }
8183
- async function uploadAvatar(config, file, userId, options) {
8184
- return uploadFile(config, file, {
8185
- ...options,
8186
- type: "avatar",
8187
- userId
8257
+ xhr.open("PUT", url);
8258
+ for (const [k, v] of Object.entries(headers)) {
8259
+ try {
8260
+ xhr.setRequestHeader(k, v);
8261
+ } catch {
8262
+ }
8263
+ }
8264
+ xhr.send(body);
8188
8265
  });
8189
8266
  }
8190
- async function deleteFile(config, fileId) {
8191
- const endpoint = storageEndpoints.delete;
8192
- await callApi(
8193
- config,
8194
- endpoint.path.replace(":id", encodeURIComponent(fileId)),
8195
- { method: endpoint.method }
8196
- );
8197
- }
8198
- async function getFileUrl(config, fileId) {
8199
- const data = await callApi(config, `/storage/files/${fileId}`, { method: "GET" });
8200
- return data.url;
8267
+ function resolveXhrEtag(xhr) {
8268
+ const canonical = xhr.getResponseHeader("ETag");
8269
+ if (canonical !== null) return canonical;
8270
+ return xhr.getResponseHeader("etag") ?? "";
8201
8271
  }
8202
- async function getFileInfo(config, fileId) {
8203
- const endpoint = storageEndpoints.get;
8204
- return callApi(config, endpoint.path.replace(":id", encodeURIComponent(fileId)), {
8205
- method: endpoint.method
8272
+ async function putBlobFetch(url, body, headers, signal, onProgress) {
8273
+ let stream = body;
8274
+ if (onProgress && typeof TransformStream !== "undefined" && typeof body.stream === "function") {
8275
+ let loaded = 0;
8276
+ const counter = new TransformStream({
8277
+ transform(chunk, controller) {
8278
+ loaded += chunk.byteLength;
8279
+ onProgress(loaded);
8280
+ controller.enqueue(chunk);
8281
+ }
8282
+ });
8283
+ stream = body.stream().pipeThrough(counter);
8284
+ }
8285
+ const res = await fetch(url, {
8286
+ method: "PUT",
8287
+ body: stream,
8288
+ headers,
8289
+ signal,
8290
+ // Required when streaming a request body in Chromium / undici.
8291
+ // Cast: TS lib lacks `duplex`.
8292
+ ...{ duplex: "half" }
8206
8293
  });
8294
+ if (!res.ok) {
8295
+ const err = new Error(`PUT failed with status ${res.status}`);
8296
+ err.status = res.status;
8297
+ throw err;
8298
+ }
8299
+ const etag = resolveFetchEtag(res.headers);
8300
+ if (onProgress) onProgress(body.size);
8301
+ return { etag: stripQuotes(etag), status: res.status };
8207
8302
  }
8208
- async function listFileVersions(config, fileId) {
8209
- const data = await callApi(
8210
- config,
8211
- `/storage/files/${encodeURIComponent(fileId)}/versions`,
8212
- { method: "GET" }
8213
- );
8214
- return data.versions;
8303
+ function resolveFetchEtag(headers) {
8304
+ const lowerCase = headers.get("etag");
8305
+ if (lowerCase !== null) return lowerCase;
8306
+ return headers.get("ETag") ?? "";
8215
8307
  }
8216
- async function restoreFileVersion(config, fileId, versionId) {
8217
- const data = await callApi(
8218
- config,
8219
- `/storage/files/${encodeURIComponent(fileId)}/versions/${encodeURIComponent(versionId)}/restore`,
8220
- { method: "POST" }
8221
- );
8222
- return data.version;
8308
+ function stripQuotes(s) {
8309
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1);
8310
+ return s;
8223
8311
  }
8224
- async function softDeleteFile(config, fileId) {
8225
- await callApi(config, `/storage/files/${encodeURIComponent(fileId)}`, {
8226
- method: "DELETE"
8312
+ async function putWithRetry(url, body, headers, signal, onProgress) {
8313
+ let lastErr;
8314
+ const maxRetries = 5;
8315
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
8316
+ if (signal?.aborted) throw toAbortError();
8317
+ try {
8318
+ return await putBlob(url, body, headers, signal, onProgress);
8319
+ } catch (err) {
8320
+ if (isAbortError(err)) throw err;
8321
+ lastErr = err;
8322
+ if (attempt === maxRetries || !isRetryableError2(err)) throw err;
8323
+ await sleep(calculateBackoffDelay(attempt), signal);
8324
+ }
8325
+ }
8326
+ throw lastErr;
8327
+ }
8328
+ async function uploadsCreate(config, blob, options) {
8329
+ const signal = options.signal;
8330
+ if (signal?.aborted) throw toAbortError();
8331
+ const filename = options.filename ?? (isFile(blob) ? blob.name : void 0) ?? "upload.bin";
8332
+ const contentType = options.contentType ?? (blob.type && blob.type.length > 0 ? blob.type : "application/octet-stream");
8333
+ const size = blob.size;
8334
+ const checksumSha256 = options.checksumSha256 ?? await computeSha256Hex(blob);
8335
+ const idempotencyKey = options.idempotencyKey ?? uuidv7();
8336
+ const createBody = {
8337
+ filename,
8338
+ contentType,
8339
+ size,
8340
+ folder: options.folder,
8341
+ visibility: options.visibility,
8342
+ metadata: options.metadata,
8343
+ checksumSha256,
8344
+ ifNoneMatch: options.ifNoneMatch
8345
+ };
8346
+ const session = await callApi(config, PATHS.uploads, {
8347
+ method: "POST",
8348
+ body: createBody,
8349
+ idempotencyKey,
8350
+ signal
8227
8351
  });
8352
+ const uploadId = session.uploadId;
8353
+ const onAborted = async () => {
8354
+ try {
8355
+ await callApi(config, PATHS.upload(uploadId), { method: "DELETE" });
8356
+ } catch {
8357
+ }
8358
+ clearResume(String(uploadId));
8359
+ };
8360
+ try {
8361
+ if (session.method === "PUT") {
8362
+ return await runSinglePart(config, blob, session, options, idempotencyKey);
8363
+ }
8364
+ return await runMultipart(config, blob, session, options, idempotencyKey);
8365
+ } catch (err) {
8366
+ if (isAbortError(err)) {
8367
+ await onAborted();
8368
+ }
8369
+ throw err;
8370
+ }
8228
8371
  }
8229
- async function restoreFile(config, fileId) {
8230
- const data = await callApi(
8231
- config,
8232
- `/storage/files/${encodeURIComponent(fileId)}/restore`,
8233
- { method: "POST" }
8234
- );
8235
- return data.file;
8372
+ function isFile(b) {
8373
+ return typeof b.name === "string";
8236
8374
  }
8237
- async function downloadFileVersion(config, fileId, versionId) {
8238
- const signed = await callApi(
8375
+ async function runSinglePart(config, blob, session, options, idempotencyKey) {
8376
+ const { onProgress, signal } = options;
8377
+ const total = blob.size;
8378
+ const trackProgress = onProgress ? (loaded) => {
8379
+ onProgress({ loaded, total, partsCompleted: 0, partsTotal: 1 });
8380
+ } : void 0;
8381
+ const put = await putWithRetry(session.url, blob, session.headers, signal, trackProgress);
8382
+ if (onProgress) onProgress({ loaded: total, total, partsCompleted: 1, partsTotal: 1 });
8383
+ const completion = await callApi(
8239
8384
  config,
8240
- "/storage/signed-url",
8385
+ PATHS.uploadComplete(session.uploadId),
8241
8386
  {
8242
8387
  method: "POST",
8243
- body: { fileId, versionId }
8388
+ body: { parts: [{ partNumber: 1, etag: put.etag }] },
8389
+ idempotencyKey: `${idempotencyKey}:complete`,
8390
+ signal
8244
8391
  }
8245
8392
  );
8246
- const res = await fetch(signed.url);
8247
- if (!res.ok) {
8248
- throw new SylphxError(`Failed to download version payload: ${res.status}`, {
8249
- code: "BAD_REQUEST"
8393
+ clearResume(String(session.uploadId));
8394
+ return await fetchFile(config, completion.fileId);
8395
+ }
8396
+ async function runMultipart(config, blob, session, options, idempotencyKey) {
8397
+ const { onProgress, signal } = options;
8398
+ const partSize = session.partSize;
8399
+ const total = blob.size;
8400
+ const partsTotal = session.partCount;
8401
+ const completedParts = [];
8402
+ const partLoaded = /* @__PURE__ */ new Map();
8403
+ const reportProgress = () => {
8404
+ if (!onProgress) return;
8405
+ let loaded = 0;
8406
+ for (const v of partLoaded.values()) loaded += v;
8407
+ onProgress({ loaded, total, partsCompleted: completedParts.length, partsTotal });
8408
+ };
8409
+ for (const part of session.parts) {
8410
+ if (signal?.aborted) throw toAbortError();
8411
+ const offset = (part.partNumber - 1) * partSize;
8412
+ const end = Math.min(offset + partSize, total);
8413
+ const slice = blob.slice(offset, end);
8414
+ const trackProgress = onProgress ? (loaded) => {
8415
+ partLoaded.set(part.partNumber, loaded);
8416
+ reportProgress();
8417
+ } : void 0;
8418
+ const result = await putWithRetry(part.url, slice, {}, signal, trackProgress);
8419
+ completedParts.push({ partNumber: part.partNumber, etag: result.etag });
8420
+ partLoaded.set(part.partNumber, slice.size);
8421
+ reportProgress();
8422
+ persistResume({
8423
+ uploadId: String(session.uploadId),
8424
+ completedParts: [...completedParts],
8425
+ updatedAt: Date.now()
8250
8426
  });
8251
8427
  }
8252
- return res.blob();
8428
+ const completion = await callApi(
8429
+ config,
8430
+ PATHS.uploadComplete(session.uploadId),
8431
+ {
8432
+ method: "POST",
8433
+ body: { parts: completedParts },
8434
+ idempotencyKey: `${idempotencyKey}:complete`,
8435
+ signal
8436
+ }
8437
+ );
8438
+ clearResume(String(session.uploadId));
8439
+ return await fetchFile(config, completion.fileId);
8253
8440
  }
8254
- async function getSignedUrl(config, fileId, options) {
8255
- return callApi(config, "/storage/signed-url", {
8256
- method: "POST",
8257
- body: {
8258
- fileId,
8259
- ...options
8441
+ async function fetchFile(config, fileId) {
8442
+ return callApi(config, PATHS.file(fileId), { method: "GET" });
8443
+ }
8444
+ async function uploadsAbort(config, uploadId) {
8445
+ await withRetry(() => callApi(config, PATHS.upload(uploadId), { method: "DELETE" }), {
8446
+ maxRetries: 3
8447
+ });
8448
+ clearResume(String(uploadId));
8449
+ }
8450
+ function filesListPage(config, options, cursor) {
8451
+ const query = {
8452
+ folder: options.folder,
8453
+ cursor: cursor ?? options.cursor,
8454
+ limit: options.limit,
8455
+ includeDeleted: options.includeDeleted
8456
+ };
8457
+ return callApi(config, PATHS.files, {
8458
+ method: "GET",
8459
+ query: {
8460
+ folder: query.folder,
8461
+ cursor: query.cursor,
8462
+ limit: query.limit,
8463
+ includeDeleted: query.includeDeleted
8260
8464
  }
8261
8465
  });
8262
8466
  }
8467
+ function filesList(config, options = {}) {
8468
+ const fetchPage = (cursor) => filesListPage(config, options, cursor);
8469
+ const iter = {
8470
+ [Symbol.asyncIterator]: async function* () {
8471
+ let cursor = options.cursor;
8472
+ do {
8473
+ const page2 = await fetchPage(cursor ?? void 0);
8474
+ for (const f of page2.files) yield f;
8475
+ cursor = page2.nextCursor;
8476
+ } while (cursor);
8477
+ }
8478
+ };
8479
+ return Object.assign(iter, { fetchPage });
8480
+ }
8481
+ async function filesGet(config, fileId) {
8482
+ return callApi(config, PATHS.file(fileId), { method: "GET" });
8483
+ }
8484
+ async function filesDelete(config, fileId) {
8485
+ return callApi(config, PATHS.file(fileId), { method: "DELETE" });
8486
+ }
8487
+ async function filesRestore(config, fileId) {
8488
+ return callApi(config, PATHS.fileRestore(fileId), { method: "POST" });
8489
+ }
8490
+ async function filesSignedUrl(config, fileId, options = {}) {
8491
+ const body = {
8492
+ expiresIn: options.expiresIn,
8493
+ disposition: options.disposition,
8494
+ userId: options.userId
8495
+ };
8496
+ return callApi(config, PATHS.fileSignedUrl(fileId), {
8497
+ method: "POST",
8498
+ body
8499
+ });
8500
+ }
8501
+ async function filesCopy(config, fileId, options) {
8502
+ const body = {
8503
+ folder: options.folder,
8504
+ filename: options.filename,
8505
+ visibility: options.visibility,
8506
+ metadata: options.metadata
8507
+ };
8508
+ return callApi(config, PATHS.fileCopy(fileId), {
8509
+ method: "POST",
8510
+ body
8511
+ });
8512
+ }
8513
+ async function filesVersionsList(config, fileId) {
8514
+ const data = await callApi(config, PATHS.versions(fileId), {
8515
+ method: "GET"
8516
+ });
8517
+ return data.versions;
8518
+ }
8519
+ async function filesVersionsRestore(config, fileId, versionId) {
8520
+ return callApi(config, PATHS.versionRestore(fileId, versionId), { method: "POST" });
8521
+ }
8522
+ var storage = {
8523
+ uploads: {
8524
+ create: uploadsCreate,
8525
+ abort: uploadsAbort
8526
+ },
8527
+ files: {
8528
+ list: filesList,
8529
+ get: filesGet,
8530
+ delete: filesDelete,
8531
+ restore: filesRestore,
8532
+ signedUrl: filesSignedUrl,
8533
+ copy: filesCopy,
8534
+ versions: {
8535
+ list: filesVersionsList,
8536
+ restore: filesVersionsRestore
8537
+ }
8538
+ }
8539
+ };
8263
8540
 
8264
8541
  // src/lib/notifications/service-worker.ts
8265
8542
  function initPushServiceWorker(config = {}) {
@@ -10706,7 +10983,6 @@ export {
10706
10983
  deleteCron,
10707
10984
  deleteDocument,
10708
10985
  deleteEnvVar,
10709
- deleteFile,
10710
10986
  deleteOrganization,
10711
10987
  deletePasskey,
10712
10988
  deletePermission,
@@ -10718,7 +10994,6 @@ export {
10718
10994
  disableDebug,
10719
10995
  disableTwoFactor,
10720
10996
  disconnectOAuthProvider,
10721
- downloadFileVersion,
10722
10997
  dpop,
10723
10998
  embed,
10724
10999
  enableDebug,
@@ -10751,8 +11026,6 @@ export {
10751
11026
  getErrorCode,
10752
11027
  getErrorMessage,
10753
11028
  getFacets,
10754
- getFileInfo,
10755
- getFileUrl,
10756
11029
  getFlagPayload,
10757
11030
  getFlags,
10758
11031
  getLeaderboard,
@@ -10779,7 +11052,6 @@ export {
10779
11052
  getSecrets,
10780
11053
  getSecurityScore,
10781
11054
  getSession,
10782
- getSignedUrl,
10783
11055
  getStreak,
10784
11056
  getSubscription,
10785
11057
  getTask,
@@ -10837,7 +11109,6 @@ export {
10837
11109
  leaveOrganization,
10838
11110
  linkAnonymousConsents,
10839
11111
  listEnvVars,
10840
- listFileVersions,
10841
11112
  listOAuthProviders,
10842
11113
  listPasskeys,
10843
11114
  listPermissions,
@@ -10885,8 +11156,6 @@ export {
10885
11156
  resetPassword,
10886
11157
  resetPlatformCookieCache,
10887
11158
  resetPlatformJwksCache,
10888
- restoreFile,
10889
- restoreFileVersion,
10890
11159
  resumeCron,
10891
11160
  revokeAllTokens,
10892
11161
  revokeOrganizationInvitation,
@@ -10908,8 +11177,8 @@ export {
10908
11177
  signIn,
10909
11178
  signOut,
10910
11179
  signUp,
10911
- softDeleteFile,
10912
11180
  startPasskeyRegistration,
11181
+ storage,
10913
11182
  streamToString,
10914
11183
  submitScore,
10915
11184
  suspendUser,
@@ -10930,8 +11199,6 @@ export {
10930
11199
  updateUserMetadata,
10931
11200
  updateUserProfile,
10932
11201
  updateWebhookConfig,
10933
- uploadAvatar,
10934
- uploadFile,
10935
11202
  upsertDocument,
10936
11203
  user,
10937
11204
  userInfo,