@sylphx/sdk 0.8.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
@@ -5921,6 +5921,14 @@ async function forgotPassword(config, email) {
5921
5921
  body: { email }
5922
5922
  });
5923
5923
  }
5924
+ async function resendVerificationEmail(config, email) {
5925
+ const endpoint = authEndpoints.resendEmailVerification;
5926
+ const body = { email };
5927
+ await callApi(config, endpoint.path, {
5928
+ method: endpoint.method,
5929
+ body
5930
+ });
5931
+ }
5924
5932
  async function resetPassword(config, input) {
5925
5933
  await callApi(config, "/auth/reset-password", {
5926
5934
  method: "POST",
@@ -7994,169 +8002,248 @@ async function getBillingUsage(config, options) {
7994
8002
  });
7995
8003
  }
7996
8004
 
7997
- // src/storage.ts
7998
- import { storageEndpoints } from "@sylphx/contract";
8005
+ // src/lib/retry.ts
7999
8006
  init_constants();
8000
- init_errors();
8001
- var UPLOAD_RETRY_CONFIG = {
8002
- /** Maximum number of retry attempts (AWS S3 pattern) */
8007
+ var DEFAULT_RETRY_CONFIG = {
8003
8008
  maxRetries: 5,
8004
- /** Base delay in milliseconds */
8005
8009
  baseDelayMs: BASE_RETRY_DELAY_MS,
8006
- /** Maximum delay cap in milliseconds */
8007
- maxDelayMs: MAX_RETRY_DELAY_MS,
8008
- /** Jitter type: 'full' for full jitter (AWS recommended) */
8009
- jitter: "full"
8010
+ maxDelayMs: MAX_RETRY_DELAY_MS
8010
8011
  };
8011
- function calculateBackoffDelay(attempt) {
8012
- const { baseDelayMs, maxDelayMs } = UPLOAD_RETRY_CONFIG;
8013
- const exponentialDelay = baseDelayMs * 2 ** attempt;
8014
- const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
8015
- 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;
8016
8016
  }
8017
- async function sleep(ms, signal) {
8017
+ function sleep(ms, signal) {
8018
8018
  return new Promise((resolve, reject) => {
8019
8019
  if (signal?.aborted) {
8020
- reject(new DOMException("Upload aborted", "AbortError"));
8020
+ reject(toAbortError());
8021
8021
  return;
8022
8022
  }
8023
- const timeoutId = setTimeout(resolve, ms);
8023
+ const timer = setTimeout(resolve, ms);
8024
8024
  signal?.addEventListener(
8025
8025
  "abort",
8026
8026
  () => {
8027
- clearTimeout(timeoutId);
8028
- reject(new DOMException("Upload aborted", "AbortError"));
8027
+ clearTimeout(timer);
8028
+ reject(toAbortError());
8029
8029
  },
8030
8030
  { once: true }
8031
8031
  );
8032
8032
  });
8033
8033
  }
8034
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
8034
8035
  function isRetryableError2(error) {
8035
- if (error instanceof DOMException && error.name === "AbortError") {
8036
- return false;
8037
- }
8038
- if (error instanceof TypeError) {
8039
- return true;
8040
- }
8036
+ if (isAbortError(error)) return false;
8037
+ if (error instanceof TypeError) return true;
8041
8038
  if (error instanceof Error && "status" in error) {
8042
8039
  const status = error.status;
8043
- return status >= 500 || status === 429;
8040
+ return status >= 500 || RETRYABLE_STATUSES.has(status);
8044
8041
  }
8045
8042
  return false;
8046
8043
  }
8047
- async function uploadFile(config, file, options) {
8048
- const { signal } = options ?? {};
8049
- if (signal?.aborted) {
8050
- 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");
8051
8052
  }
8052
- let tokenResponse = null;
8053
- let lastError = null;
8054
- 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");
8055
8066
  try {
8056
- tokenResponse = await fetch(buildApiUrl(config, "/storage/upload"), {
8057
- method: "POST",
8058
- headers: buildHeaders(config),
8059
- body: JSON.stringify({
8060
- filename: file.name,
8061
- contentType: file.type,
8062
- size: file.size,
8063
- path: options?.path,
8064
- type: options?.type ?? "file",
8065
- userId: options?.userId
8066
- }),
8067
- signal
8068
- });
8069
- if (tokenResponse.ok) {
8070
- break;
8071
- }
8072
- if (tokenResponse.status >= 500 || tokenResponse.status === 429) {
8073
- if (attempt < UPLOAD_RETRY_CONFIG.maxRetries) {
8074
- const delay = calculateBackoffDelay(attempt);
8075
- await sleep(delay, signal);
8076
- continue;
8077
- }
8078
- }
8079
- const error = await tokenResponse.json().catch(() => ({ message: "Failed to get upload token" }));
8080
- throw new SylphxError(error.message ?? "Failed to get upload token", {
8081
- code: "BAD_REQUEST"
8082
- });
8083
- } catch (error) {
8084
- if (error instanceof DOMException && error.name === "AbortError") {
8085
- throw error;
8086
- }
8087
- lastError = error instanceof Error ? error : new Error(String(error));
8088
- if (isRetryableError2(error) && attempt < UPLOAD_RETRY_CONFIG.maxRetries) {
8089
- const delay = calculateBackoffDelay(attempt);
8090
- await sleep(delay, signal);
8091
- continue;
8092
- }
8093
- 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 {
8094
8164
  }
8165
+ return;
8095
8166
  }
8096
- if (!tokenResponse?.ok) {
8097
- throw lastError ?? new SylphxError("Failed to get upload token after retries", {
8098
- code: "BAD_REQUEST"
8099
- });
8167
+ if (!isBrowser()) {
8168
+ void persistResumeNode(rec);
8100
8169
  }
8101
- const { uploadUrl, publicUrl } = await tokenResponse.json();
8102
- return executeUploadWithRetry(file, uploadUrl, publicUrl, options);
8103
8170
  }
8104
- async function executeUploadWithRetry(file, uploadUrl, publicUrl, options) {
8105
- const { signal } = options ?? {};
8106
- let lastError = null;
8107
- for (let attempt = 0; attempt <= UPLOAD_RETRY_CONFIG.maxRetries; attempt++) {
8171
+ function clearResume(uploadId) {
8172
+ if (hasLocalStorage()) {
8108
8173
  try {
8109
- return await executeUpload(file, uploadUrl, publicUrl, options);
8110
- } catch (error) {
8111
- if (error instanceof DOMException && error.name === "AbortError") {
8112
- throw error;
8113
- }
8114
- lastError = error instanceof Error ? error : new Error(String(error));
8115
- if (isRetryableError2(error) && attempt < UPLOAD_RETRY_CONFIG.maxRetries) {
8116
- const delay = calculateBackoffDelay(attempt);
8117
- await sleep(delay, signal);
8118
- continue;
8119
- }
8120
- throw lastError;
8174
+ localStorage.removeItem(resumeKey(uploadId));
8175
+ } catch {
8121
8176
  }
8177
+ return;
8122
8178
  }
8123
- throw lastError ?? new Error("Upload failed after retries");
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 = {};
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 {
8212
+ }
8213
+ }
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);
8124
8217
  }
8125
- function executeUpload(file, uploadUrl, publicUrl, options) {
8126
- const { signal, onProgress } = options ?? {};
8218
+ function putBlobXhr(url, body, headers, signal, onProgress) {
8127
8219
  return new Promise((resolve, reject) => {
8128
8220
  const xhr = new XMLHttpRequest();
8129
8221
  const handleAbort = () => {
8130
- xhr.abort();
8131
- reject(new DOMException("Upload aborted", "AbortError"));
8222
+ try {
8223
+ xhr.abort();
8224
+ } catch {
8225
+ }
8226
+ reject(toAbortError("Upload aborted"));
8132
8227
  };
8133
8228
  if (signal?.aborted) {
8134
- reject(new DOMException("Upload aborted", "AbortError"));
8229
+ reject(toAbortError("Upload aborted"));
8135
8230
  return;
8136
8231
  }
8137
8232
  signal?.addEventListener("abort", handleAbort, { once: true });
8138
- xhr.upload.addEventListener("progress", (event) => {
8139
- if (event.lengthComputable && onProgress) {
8140
- onProgress({
8141
- loaded: event.loaded,
8142
- total: event.total,
8143
- progress: Math.round(event.loaded / event.total * 100)
8144
- });
8145
- }
8146
- });
8233
+ if (onProgress) {
8234
+ xhr.upload.addEventListener("progress", (e) => {
8235
+ if (e.lengthComputable) onProgress(e.loaded);
8236
+ });
8237
+ }
8147
8238
  xhr.addEventListener("load", () => {
8148
8239
  signal?.removeEventListener("abort", handleAbort);
8149
8240
  if (xhr.status >= 200 && xhr.status < 300) {
8150
- resolve({
8151
- url: publicUrl,
8152
- pathname: options?.path ? `${options.path}/${file.name}` : file.name,
8153
- contentType: file.type,
8154
- size: file.size
8155
- });
8241
+ const etag = resolveXhrEtag(xhr);
8242
+ resolve({ etag: stripQuotes(etag), status: xhr.status });
8156
8243
  } else {
8157
- const error = new Error(`Upload failed with status ${xhr.status}`);
8158
- error.status = xhr.status;
8159
- reject(error);
8244
+ const err = new Error(`PUT failed with status ${xhr.status}`);
8245
+ err.status = xhr.status;
8246
+ reject(err);
8160
8247
  }
8161
8248
  });
8162
8249
  xhr.addEventListener("error", () => {
@@ -8165,93 +8252,291 @@ function executeUpload(file, uploadUrl, publicUrl, options) {
8165
8252
  });
8166
8253
  xhr.addEventListener("abort", () => {
8167
8254
  signal?.removeEventListener("abort", handleAbort);
8168
- reject(new DOMException("Upload aborted", "AbortError"));
8255
+ reject(toAbortError("Upload aborted"));
8169
8256
  });
8170
- xhr.open("PUT", uploadUrl);
8171
- xhr.setRequestHeader("Content-Type", file.type);
8172
- xhr.send(file);
8173
- });
8174
- }
8175
- async function uploadAvatar(config, file, userId, options) {
8176
- return uploadFile(config, file, {
8177
- ...options,
8178
- type: "avatar",
8179
- 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);
8180
8265
  });
8181
8266
  }
8182
- async function deleteFile(config, fileId) {
8183
- const endpoint = storageEndpoints.delete;
8184
- await callApi(
8185
- config,
8186
- endpoint.path.replace(":id", encodeURIComponent(fileId)),
8187
- { method: endpoint.method }
8188
- );
8189
- }
8190
- async function getFileUrl(config, fileId) {
8191
- const data = await callApi(config, `/storage/files/${fileId}`, { method: "GET" });
8192
- return data.url;
8267
+ function resolveXhrEtag(xhr) {
8268
+ const canonical = xhr.getResponseHeader("ETag");
8269
+ if (canonical !== null) return canonical;
8270
+ return xhr.getResponseHeader("etag") ?? "";
8193
8271
  }
8194
- async function getFileInfo(config, fileId) {
8195
- const endpoint = storageEndpoints.get;
8196
- return callApi(config, endpoint.path.replace(":id", encodeURIComponent(fileId)), {
8197
- 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" }
8198
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 };
8199
8302
  }
8200
- async function listFileVersions(config, fileId) {
8201
- const data = await callApi(
8202
- config,
8203
- `/storage/files/${encodeURIComponent(fileId)}/versions`,
8204
- { method: "GET" }
8205
- );
8206
- return data.versions;
8303
+ function resolveFetchEtag(headers) {
8304
+ const lowerCase = headers.get("etag");
8305
+ if (lowerCase !== null) return lowerCase;
8306
+ return headers.get("ETag") ?? "";
8207
8307
  }
8208
- async function restoreFileVersion(config, fileId, versionId) {
8209
- const data = await callApi(
8210
- config,
8211
- `/storage/files/${encodeURIComponent(fileId)}/versions/${encodeURIComponent(versionId)}/restore`,
8212
- { method: "POST" }
8213
- );
8214
- return data.version;
8308
+ function stripQuotes(s) {
8309
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1);
8310
+ return s;
8215
8311
  }
8216
- async function softDeleteFile(config, fileId) {
8217
- await callApi(config, `/storage/files/${encodeURIComponent(fileId)}`, {
8218
- 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
8219
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
+ }
8220
8371
  }
8221
- async function restoreFile(config, fileId) {
8222
- const data = await callApi(
8223
- config,
8224
- `/storage/files/${encodeURIComponent(fileId)}/restore`,
8225
- { method: "POST" }
8226
- );
8227
- return data.file;
8372
+ function isFile(b) {
8373
+ return typeof b.name === "string";
8228
8374
  }
8229
- async function downloadFileVersion(config, fileId, versionId) {
8230
- 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(
8231
8384
  config,
8232
- "/storage/signed-url",
8385
+ PATHS.uploadComplete(session.uploadId),
8233
8386
  {
8234
8387
  method: "POST",
8235
- body: { fileId, versionId }
8388
+ body: { parts: [{ partNumber: 1, etag: put.etag }] },
8389
+ idempotencyKey: `${idempotencyKey}:complete`,
8390
+ signal
8236
8391
  }
8237
8392
  );
8238
- const res = await fetch(signed.url);
8239
- if (!res.ok) {
8240
- throw new SylphxError(`Failed to download version payload: ${res.status}`, {
8241
- 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()
8242
8426
  });
8243
8427
  }
8244
- 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);
8245
8440
  }
8246
- async function getSignedUrl(config, fileId, options) {
8247
- return callApi(config, "/storage/signed-url", {
8248
- method: "POST",
8249
- body: {
8250
- fileId,
8251
- ...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
8464
+ }
8465
+ });
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);
8252
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
8253
8511
  });
8254
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
+ };
8255
8540
 
8256
8541
  // src/lib/notifications/service-worker.ts
8257
8542
  function initPushServiceWorker(config = {}) {
@@ -10698,7 +10983,6 @@ export {
10698
10983
  deleteCron,
10699
10984
  deleteDocument,
10700
10985
  deleteEnvVar,
10701
- deleteFile,
10702
10986
  deleteOrganization,
10703
10987
  deletePasskey,
10704
10988
  deletePermission,
@@ -10710,7 +10994,6 @@ export {
10710
10994
  disableDebug,
10711
10995
  disableTwoFactor,
10712
10996
  disconnectOAuthProvider,
10713
- downloadFileVersion,
10714
10997
  dpop,
10715
10998
  embed,
10716
10999
  enableDebug,
@@ -10743,8 +11026,6 @@ export {
10743
11026
  getErrorCode,
10744
11027
  getErrorMessage,
10745
11028
  getFacets,
10746
- getFileInfo,
10747
- getFileUrl,
10748
11029
  getFlagPayload,
10749
11030
  getFlags,
10750
11031
  getLeaderboard,
@@ -10771,7 +11052,6 @@ export {
10771
11052
  getSecrets,
10772
11053
  getSecurityScore,
10773
11054
  getSession,
10774
- getSignedUrl,
10775
11055
  getStreak,
10776
11056
  getSubscription,
10777
11057
  getTask,
@@ -10829,7 +11109,6 @@ export {
10829
11109
  leaveOrganization,
10830
11110
  linkAnonymousConsents,
10831
11111
  listEnvVars,
10832
- listFileVersions,
10833
11112
  listOAuthProviders,
10834
11113
  listPasskeys,
10835
11114
  listPermissions,
@@ -10871,13 +11150,12 @@ export {
10871
11150
  replayWebhookDelivery,
10872
11151
  requestEmailChange,
10873
11152
  rescheduleEmail,
11153
+ resendVerificationEmail,
10874
11154
  resetCircuitBreaker,
10875
11155
  resetDebugModeCache,
10876
11156
  resetPassword,
10877
11157
  resetPlatformCookieCache,
10878
11158
  resetPlatformJwksCache,
10879
- restoreFile,
10880
- restoreFileVersion,
10881
11159
  resumeCron,
10882
11160
  revokeAllTokens,
10883
11161
  revokeOrganizationInvitation,
@@ -10899,8 +11177,8 @@ export {
10899
11177
  signIn,
10900
11178
  signOut,
10901
11179
  signUp,
10902
- softDeleteFile,
10903
11180
  startPasskeyRegistration,
11181
+ storage,
10904
11182
  streamToString,
10905
11183
  submitScore,
10906
11184
  suspendUser,
@@ -10921,8 +11199,6 @@ export {
10921
11199
  updateUserMetadata,
10922
11200
  updateUserProfile,
10923
11201
  updateWebhookConfig,
10924
- uploadAvatar,
10925
- uploadFile,
10926
11202
  upsertDocument,
10927
11203
  user,
10928
11204
  userInfo,