@sylphx/sdk 0.9.0 → 0.10.1

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
@@ -4783,7 +4783,7 @@ var init_webapi = __esm({
4783
4783
  // src/connection-url.ts
4784
4784
  var SYLPHX_PROTOCOL = "sylphx:";
4785
4785
  var DEFAULT_VERSION = "v1";
4786
- var CREDENTIAL_REGEX = /^(pk|sk)_(dev|stg|prod|prev)_[a-f0-9]{32,64}$/;
4786
+ var CREDENTIAL_REGEX = /^(pk|sk)_(dev|stg|prod|prev)(?:_[a-z0-9]{12})?_[a-f0-9]{32,64}$/;
4787
4787
  var VERSION_REGEX = /^v[0-9]+$/;
4788
4788
  var SLUG_REGEX = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
4789
4789
  var InvalidConnectionUrlError = class _InvalidConnectionUrlError extends Error {
@@ -4800,7 +4800,7 @@ function fail(reason) {
4800
4800
  function parseCredential(raw) {
4801
4801
  const match = CREDENTIAL_REGEX.exec(raw);
4802
4802
  if (!match) {
4803
- fail(`credential must match (pk|sk)_(dev|stg|prod|prev)_[a-f0-9]{32,64}, got "${raw}"`);
4803
+ fail(`credential must match (pk|sk)_(dev|stg|prod|prev)(_{ref})?_{hex}, got "${raw}"`);
4804
4804
  }
4805
4805
  return {
4806
4806
  credentialType: match[1],
@@ -4879,7 +4879,7 @@ init_constants();
4879
4879
  init_errors();
4880
4880
  var LEGACY_EMBEDDED_REF_PATTERN = /^(pk|sk)_(dev|stg|prod|prev)_[a-z0-9]{12}_[a-f0-9]+$/;
4881
4881
  var LEGACY_APP_KEY_PATTERN = /^app_(dev|stg|prod|prev)_/;
4882
- var MIGRATION_MESSAGE = "API key format has changed. Use a sylphx:// connection URL instead.\n\nNew format: sylphx://pk_prod_{hex}@your-slug.api.sylphx.com\n\nGenerate new credentials from the Sylphx Console \u2192 Your App \u2192 Environments.\nSee https://docs.sylphx.com/migration for details.";
4882
+ var MIGRATION_MESSAGE = "API key format has changed. Use a sylphx:// connection URL instead.\n\nNew format: sylphx://pk_prod_{ref?}_{hex}@your-slug.api.sylphx.com\n\nGenerate new credentials from the Sylphx Console \u2192 Your App \u2192 Environments.\nSee https://docs.sylphx.com/migration for details.";
4883
4883
  function rejectLegacyKeyFormat(input) {
4884
4884
  const trimmed = input.trim().toLowerCase();
4885
4885
  if (LEGACY_APP_KEY_PATTERN.test(trimmed)) {
@@ -5012,7 +5012,7 @@ function createConfigFromComponents(input) {
5012
5012
  });
5013
5013
  }
5014
5014
  throw new SylphxError(
5015
- `[Sylphx] Invalid credential format. Expected (pk|sk)_(dev|stg|prod|prev)_[a-f0-9]{32,64}. Got: "${trimmedCred.slice(0, 30)}..."`,
5015
+ `[Sylphx] Invalid credential format. Expected (pk|sk)_(dev|stg|prod|prev)(_{ref})?_{hex}. Got: "${trimmedCred.slice(0, 30)}..."`,
5016
5016
  { code: "BAD_REQUEST" }
5017
5017
  );
5018
5018
  }
@@ -5291,11 +5291,12 @@ init_constants();
5291
5291
  init_errors();
5292
5292
 
5293
5293
  // src/key-validation.ts
5294
- var SECRET_KEY_PATTERN = /^sk_(dev|stg|prod)_[a-z0-9_-]+$/;
5294
+ var SECRET_KEY_PATTERN = /^sk_(dev|stg|prod|prev)_[a-z0-9_-]+$/;
5295
5295
  var ENV_PREFIX_MAP = {
5296
5296
  dev: "development",
5297
5297
  stg: "staging",
5298
- prod: "production"
5298
+ prod: "production",
5299
+ prev: "preview"
5299
5300
  };
5300
5301
  function detectKeyIssues(key) {
5301
5302
  const issues = [];
@@ -5320,7 +5321,7 @@ The SDK will automatically sanitize the key, but fixing the source is recommende
5320
5321
  }
5321
5322
  function createInvalidKeyError(keyType, key, envVarName) {
5322
5323
  const maskedKey = key.length > 20 ? `${key.slice(0, 20)}...` : key;
5323
- const formatHint = keyType === "appId" ? "pk_(dev|stg|prod)_{ref}_{hex} or app_(dev|stg|prod)_[id]" : "sk_(dev|stg|prod)_{ref}_{hex}";
5324
+ const formatHint = keyType === "appId" ? "pk_(dev|stg|prod|prev)_{ref}_{hex} or app_(dev|stg|prod|prev)_[id]" : "sk_(dev|stg|prod|prev)_{ref}_{hex}";
5324
5325
  const keyTypeName = keyType === "appId" ? "App ID" : "Secret Key";
5325
5326
  return `[Sylphx] Invalid ${keyTypeName} format.
5326
5327
 
@@ -5333,7 +5334,7 @@ You can find your keys in the Sylphx Console \u2192 API Keys.
5333
5334
  Common issues:
5334
5335
  \u2022 Key has uppercase characters (must be lowercase)
5335
5336
  \u2022 Key has wrong prefix (App ID: pk_ or app_, Secret Key: sk_)
5336
- \u2022 Key has invalid environment (must be dev, stg, or prod)
5337
+ \u2022 Key has invalid environment (must be dev, stg, prod, or prev)
5337
5338
  \u2022 Key was copied with extra whitespace`;
5338
5339
  }
5339
5340
  function extractEnvironment(key) {
@@ -5380,7 +5381,7 @@ function validateKeyForType(key, keyType, pattern, envVarName) {
5380
5381
  };
5381
5382
  }
5382
5383
  function validateSecretKey(key) {
5383
- return validateKeyForType(key, "secret", SECRET_KEY_PATTERN, "SYLPHX_SECRET_KEY");
5384
+ return validateKeyForType(key, "secret", SECRET_KEY_PATTERN, "secret credential");
5384
5385
  }
5385
5386
  function validateAndSanitizeSecretKey(key) {
5386
5387
  const result = validateSecretKey(key);
@@ -5402,7 +5403,14 @@ async function runPipeline(middlewares, initial) {
5402
5403
  if (next) request = next;
5403
5404
  }
5404
5405
  }
5405
- let response = await fetch(request);
5406
+ const fetchWithMiddleware = middlewares.reduceRight(
5407
+ (next, mw) => mw.onFetch ? async (request2) => {
5408
+ const response2 = await mw.onFetch?.({ request: request2, next });
5409
+ return response2 ?? next(request2);
5410
+ } : next,
5411
+ (request2) => fetch(request2)
5412
+ );
5413
+ let response = await fetchWithMiddleware(request);
5406
5414
  for (const mw of middlewares) {
5407
5415
  if (mw.onResponse) {
5408
5416
  const next = await mw.onResponse({ request, response });
@@ -5421,6 +5429,11 @@ function buildUrl(baseUrl, path, params) {
5421
5429
  ).toString();
5422
5430
  return `${url}?${search2}`;
5423
5431
  }
5432
+ function cloneRequestWithHeaders(request, updateHeaders) {
5433
+ const headers = new Headers(request.headers);
5434
+ updateHeaders(headers);
5435
+ return new Request(request, { headers });
5436
+ }
5424
5437
  function interpolatePath(path, pathParams) {
5425
5438
  if (!pathParams) return path;
5426
5439
  return path.replace(/\{(\w+)\}/g, (_match, key) => {
@@ -5483,66 +5496,51 @@ function buildClient(baseUrl, baseHeaders) {
5483
5496
  function createAuthMiddleware(config) {
5484
5497
  return {
5485
5498
  async onRequest({ request }) {
5486
- request.headers.set("X-SDK-Version", SDK_VERSION);
5487
- request.headers.set("X-SDK-Platform", SDK_PLATFORM);
5488
- if (config.secretKey) {
5489
- request.headers.set("x-app-secret", config.secretKey);
5490
- }
5491
- const token = config.getAccessToken?.();
5492
- if (token) {
5493
- request.headers.set("Authorization", `Bearer ${token}`);
5494
- }
5495
- return request;
5499
+ return cloneRequestWithHeaders(request, (headers) => {
5500
+ headers.set("X-SDK-Version", SDK_VERSION);
5501
+ headers.set("X-SDK-Platform", SDK_PLATFORM);
5502
+ if (config.secretKey) {
5503
+ headers.set("x-app-secret", config.secretKey);
5504
+ }
5505
+ const token = config.getAccessToken?.();
5506
+ if (token) {
5507
+ headers.set("Authorization", `Bearer ${token}`);
5508
+ }
5509
+ });
5496
5510
  }
5497
5511
  };
5498
5512
  }
5499
5513
  function isRetryableStatus(status) {
5500
5514
  return status >= 500 || status === 429;
5501
5515
  }
5502
- var inFlightRequests = /* @__PURE__ */ new Map();
5503
5516
  async function getRequestKey(request) {
5504
5517
  const body = request.body ? await request.clone().text() : "";
5505
5518
  return `${request.method}:${request.url}:${body}`;
5506
5519
  }
5507
5520
  function createDeduplicationMiddleware(config = {}) {
5508
5521
  const { enabled = true, methods = ["GET"] } = config;
5509
- if (!enabled) {
5510
- return {
5511
- async onRequest({ request }) {
5512
- return request;
5513
- }
5514
- };
5515
- }
5522
+ if (!enabled) return {};
5523
+ const inFlightRequests = /* @__PURE__ */ new Map();
5516
5524
  return {
5517
- async onRequest({ request }) {
5518
- if (!methods.includes(request.method)) {
5519
- return request;
5525
+ async onFetch({ request, next }) {
5526
+ const method = request.method.toUpperCase();
5527
+ if (!methods.includes(method)) {
5528
+ return next(request);
5520
5529
  }
5521
5530
  const key = await getRequestKey(request);
5522
5531
  const existing = inFlightRequests.get(key);
5523
5532
  if (existing) {
5524
- const deduped = request.clone();
5525
- deduped._dedupKey = key;
5526
- return deduped;
5527
- }
5528
- ;
5529
- request._dedupKey = key;
5530
- return request;
5531
- },
5532
- async onResponse({ request, response }) {
5533
- const key = request._dedupKey;
5534
- if (!key) return response;
5535
- const existing = inFlightRequests.get(key);
5536
- if (existing && inFlightRequests.get(key) !== void 0) {
5537
5533
  const cachedResponse = await existing;
5538
5534
  return cachedResponse.clone();
5539
5535
  }
5540
- const responsePromise = Promise.resolve(response.clone());
5536
+ const responsePromise = next(request).then((response) => response.clone());
5541
5537
  inFlightRequests.set(key, responsePromise);
5542
- responsePromise.finally(() => {
5543
- setTimeout(() => inFlightRequests.delete(key), 100);
5544
- });
5545
- return response;
5538
+ try {
5539
+ const response = await responsePromise;
5540
+ return response.clone();
5541
+ } finally {
5542
+ inFlightRequests.delete(key);
5543
+ }
5546
5544
  }
5547
5545
  };
5548
5546
  }
@@ -5708,7 +5706,9 @@ function createETagMiddleware(config) {
5708
5706
  if (Date.now() - cached.timestamp > ttlMs) {
5709
5707
  etagCache.delete(cacheKey);
5710
5708
  } else {
5711
- request.headers.set("If-None-Match", cached.etag);
5709
+ return cloneRequestWithHeaders(request, (headers) => {
5710
+ headers.set("If-None-Match", cached.etag);
5711
+ });
5712
5712
  }
5713
5713
  }
5714
5714
  return request;
@@ -6000,11 +6000,28 @@ async function inviteUser(config, input) {
6000
6000
  body: input
6001
6001
  });
6002
6002
  }
6003
- async function switchOrg(config, orgId) {
6004
- return callApi(config, "/auth/switch-org", {
6003
+ function normalizeOrgScopedTokenResponse(data) {
6004
+ const accessToken = data.accessToken ?? data.access_token;
6005
+ if (!accessToken) {
6006
+ throw new Error("Invalid org-scoped token response: missing access token");
6007
+ }
6008
+ return {
6009
+ token: accessToken,
6010
+ accessToken,
6011
+ expiresIn: data.expiresIn ?? data.expires_in,
6012
+ tokenType: data.tokenType ?? data.token_type,
6013
+ user: data.user
6014
+ };
6015
+ }
6016
+ async function getOrgScopedToken(config, orgId) {
6017
+ const data = await callApi(config, "/auth/switch-org", {
6005
6018
  method: "POST",
6006
6019
  body: { orgId }
6007
6020
  });
6021
+ return normalizeOrgScopedTokenResponse(data);
6022
+ }
6023
+ async function switchOrg(config, orgId) {
6024
+ return getOrgScopedToken(config, orgId);
6008
6025
  }
6009
6026
  var device = {
6010
6027
  /**
@@ -8003,168 +8020,208 @@ async function getBillingUsage(config, options) {
8003
8020
  }
8004
8021
 
8005
8022
  // src/storage.ts
8006
- import { storageEndpoints } from "@sylphx/contract";
8007
- init_constants();
8008
8023
  init_errors();
8009
- var UPLOAD_RETRY_CONFIG = {
8010
- /** Maximum number of retry attempts (AWS S3 pattern) */
8024
+
8025
+ // src/lib/retry.ts
8026
+ init_constants();
8027
+ var DEFAULT_RETRY_CONFIG = {
8011
8028
  maxRetries: 5,
8012
- /** Base delay in milliseconds */
8013
8029
  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"
8030
+ maxDelayMs: MAX_RETRY_DELAY_MS
8018
8031
  };
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;
8032
+ function calculateBackoffDelay(attempt, config = DEFAULT_RETRY_CONFIG) {
8033
+ const exp = config.baseDelayMs * 2 ** attempt;
8034
+ const capped = Math.min(exp, config.maxDelayMs);
8035
+ return Math.random() * capped;
8024
8036
  }
8025
- async function sleep(ms, signal) {
8037
+ function sleep(ms, signal) {
8026
8038
  return new Promise((resolve, reject) => {
8027
8039
  if (signal?.aborted) {
8028
- reject(new DOMException("Upload aborted", "AbortError"));
8040
+ reject(toAbortError());
8029
8041
  return;
8030
8042
  }
8031
- const timeoutId = setTimeout(resolve, ms);
8043
+ const timer = setTimeout(resolve, ms);
8032
8044
  signal?.addEventListener(
8033
8045
  "abort",
8034
8046
  () => {
8035
- clearTimeout(timeoutId);
8036
- reject(new DOMException("Upload aborted", "AbortError"));
8047
+ clearTimeout(timer);
8048
+ reject(toAbortError());
8037
8049
  },
8038
8050
  { once: true }
8039
8051
  );
8040
8052
  });
8041
8053
  }
8054
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
8042
8055
  function isRetryableError2(error) {
8043
- if (error instanceof DOMException && error.name === "AbortError") {
8044
- return false;
8045
- }
8046
- if (error instanceof TypeError) {
8047
- return true;
8048
- }
8056
+ if (isAbortError(error)) return false;
8057
+ if (error instanceof TypeError) return true;
8049
8058
  if (error instanceof Error && "status" in error) {
8050
8059
  const status = error.status;
8051
- return status >= 500 || status === 429;
8060
+ return status >= 500 || RETRYABLE_STATUSES.has(status);
8052
8061
  }
8053
8062
  return false;
8054
8063
  }
8055
- async function uploadFile(config, file, options) {
8056
- const { signal } = options ?? {};
8057
- if (signal?.aborted) {
8058
- throw new DOMException("Upload aborted", "AbortError");
8064
+ function isAbortError(error) {
8065
+ if (error instanceof DOMException && error.name === "AbortError") return true;
8066
+ if (error instanceof Error && error.name === "AbortError") return true;
8067
+ return false;
8068
+ }
8069
+ function toAbortError(message2 = "Aborted") {
8070
+ if (typeof DOMException !== "undefined") {
8071
+ return new DOMException(message2, "AbortError");
8059
8072
  }
8060
- let tokenResponse = null;
8061
- let lastError = null;
8062
- for (let attempt = 0; attempt <= UPLOAD_RETRY_CONFIG.maxRetries; attempt++) {
8073
+ const err = new Error(message2);
8074
+ err.name = "AbortError";
8075
+ return err;
8076
+ }
8077
+ async function withRetry(fn, options = {}) {
8078
+ const cfg = {
8079
+ maxRetries: options.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries,
8080
+ baseDelayMs: options.baseDelayMs ?? DEFAULT_RETRY_CONFIG.baseDelayMs,
8081
+ maxDelayMs: options.maxDelayMs ?? DEFAULT_RETRY_CONFIG.maxDelayMs
8082
+ };
8083
+ let lastError;
8084
+ for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
8085
+ if (options.signal?.aborted) throw toAbortError("Aborted");
8063
8086
  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;
8102
- }
8087
+ return await fn();
8088
+ } catch (err) {
8089
+ lastError = err;
8090
+ if (isAbortError(err)) throw err;
8091
+ if (attempt === cfg.maxRetries) break;
8092
+ if (!isRetryableError2(err)) throw err;
8093
+ const retryAfter = extractRetryAfter(err);
8094
+ const delay = retryAfter ?? calculateBackoffDelay(attempt, cfg);
8095
+ options.onRetry?.(attempt, delay, err);
8096
+ await sleep(delay, options.signal);
8097
+ }
8098
+ }
8099
+ throw lastError;
8100
+ }
8101
+ function extractRetryAfter(err) {
8102
+ if (err && typeof err === "object" && "retryAfter" in err) {
8103
+ const v = err.retryAfter;
8104
+ if (typeof v === "number" && v > 0) return v * 1e3;
8105
+ }
8106
+ return void 0;
8107
+ }
8108
+ function uuidv7() {
8109
+ const ms = BigInt(Date.now());
8110
+ const bytes = randomBytes(16);
8111
+ bytes[0] = Number(ms >> 40n & 0xffn);
8112
+ bytes[1] = Number(ms >> 32n & 0xffn);
8113
+ bytes[2] = Number(ms >> 24n & 0xffn);
8114
+ bytes[3] = Number(ms >> 16n & 0xffn);
8115
+ bytes[4] = Number(ms >> 8n & 0xffn);
8116
+ bytes[5] = Number(ms & 0xffn);
8117
+ bytes[6] = bytes[6] & 15 | 112;
8118
+ bytes[8] = bytes[8] & 63 | 128;
8119
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
8120
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
8121
+ }
8122
+ function randomBytes(n) {
8123
+ const out = new Uint8Array(n);
8124
+ const c = globalThis.crypto;
8125
+ if (c?.getRandomValues) {
8126
+ c.getRandomValues(out);
8127
+ return out;
8128
+ }
8129
+ for (let i = 0; i < n; i++) out[i] = Math.floor(Math.random() * 256);
8130
+ return out;
8131
+ }
8132
+
8133
+ // src/storage.ts
8134
+ var PATHS = {
8135
+ uploads: "/storage/uploads",
8136
+ upload: (id) => `/storage/uploads/${encodeURIComponent(String(id))}`,
8137
+ uploadComplete: (id) => `/storage/uploads/${encodeURIComponent(String(id))}:complete`,
8138
+ uploadPart: (id, n) => `/storage/uploads/${encodeURIComponent(String(id))}/parts/${n}`,
8139
+ files: "/storage/files",
8140
+ file: (id) => `/storage/files/${encodeURIComponent(String(id))}`,
8141
+ fileRestore: (id) => `/storage/files/${encodeURIComponent(String(id))}:restore`,
8142
+ fileSignedUrl: (id) => `/storage/files/${encodeURIComponent(String(id))}:signedUrl`,
8143
+ fileCopy: (id) => `/storage/files/${encodeURIComponent(String(id))}:copy`,
8144
+ versions: (id) => `/storage/files/${encodeURIComponent(String(id))}/versions`,
8145
+ versionRestore: (id, vid) => `/storage/files/${encodeURIComponent(String(id))}/versions/${encodeURIComponent(String(vid))}:restore`
8146
+ };
8147
+ var hasXhr = () => typeof globalThis.XMLHttpRequest !== "undefined";
8148
+ var hasLocalStorage = () => {
8149
+ try {
8150
+ const ls = globalThis.localStorage;
8151
+ return Boolean(ls && typeof ls.getItem === "function");
8152
+ } catch {
8153
+ return false;
8103
8154
  }
8104
- if (!tokenResponse?.ok) {
8105
- throw lastError ?? new SylphxError("Failed to get upload token after retries", {
8106
- code: "BAD_REQUEST"
8155
+ };
8156
+ async function computeSha256Hex(blob) {
8157
+ const subtle = globalThis.crypto?.subtle;
8158
+ if (!subtle?.digest) {
8159
+ throw new SylphxError("Web Crypto SHA-256 support is required for storage uploads", {
8160
+ code: "NOT_IMPLEMENTED"
8107
8161
  });
8108
8162
  }
8109
- const { uploadUrl, publicUrl } = await tokenResponse.json();
8110
- return executeUploadWithRetry(file, uploadUrl, publicUrl, options);
8163
+ const buf = await blob.arrayBuffer();
8164
+ const digest2 = await subtle.digest("SHA-256", buf);
8165
+ return bytesToHex(new Uint8Array(digest2));
8111
8166
  }
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++) {
8167
+ function bytesToHex(b) {
8168
+ let s = "";
8169
+ for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, "0");
8170
+ return s;
8171
+ }
8172
+ var RESUME_KEY_PREFIX = "sylphx_upload_";
8173
+ function resumeKey(uploadId) {
8174
+ return `${RESUME_KEY_PREFIX}${uploadId}`;
8175
+ }
8176
+ function persistResume(rec) {
8177
+ if (hasLocalStorage()) {
8116
8178
  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;
8179
+ localStorage.setItem(resumeKey(rec.uploadId), JSON.stringify(rec));
8180
+ } catch {
8129
8181
  }
8130
8182
  }
8131
- throw lastError ?? new Error("Upload failed after retries");
8132
8183
  }
8133
- function executeUpload(file, uploadUrl, publicUrl, options) {
8134
- const { signal, onProgress } = options ?? {};
8184
+ function clearResume(uploadId) {
8185
+ if (hasLocalStorage()) {
8186
+ try {
8187
+ localStorage.removeItem(resumeKey(uploadId));
8188
+ } catch {
8189
+ }
8190
+ }
8191
+ }
8192
+ function putBlob(url, body, headers, signal, onProgress) {
8193
+ if (hasXhr()) return putBlobXhr(url, body, headers, signal, onProgress);
8194
+ return putBlobFetch(url, body, headers, signal, onProgress);
8195
+ }
8196
+ function putBlobXhr(url, body, headers, signal, onProgress) {
8135
8197
  return new Promise((resolve, reject) => {
8136
8198
  const xhr = new XMLHttpRequest();
8137
8199
  const handleAbort = () => {
8138
- xhr.abort();
8139
- reject(new DOMException("Upload aborted", "AbortError"));
8200
+ try {
8201
+ xhr.abort();
8202
+ } catch {
8203
+ }
8204
+ reject(toAbortError("Upload aborted"));
8140
8205
  };
8141
8206
  if (signal?.aborted) {
8142
- reject(new DOMException("Upload aborted", "AbortError"));
8207
+ reject(toAbortError("Upload aborted"));
8143
8208
  return;
8144
8209
  }
8145
8210
  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
- });
8211
+ if (onProgress) {
8212
+ xhr.upload.addEventListener("progress", (e) => {
8213
+ if (e.lengthComputable) onProgress(e.loaded);
8214
+ });
8215
+ }
8155
8216
  xhr.addEventListener("load", () => {
8156
8217
  signal?.removeEventListener("abort", handleAbort);
8157
8218
  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
- });
8219
+ const etag = resolveXhrEtag(xhr);
8220
+ resolve({ etag: stripQuotes(etag), status: xhr.status });
8164
8221
  } else {
8165
- const error = new Error(`Upload failed with status ${xhr.status}`);
8166
- error.status = xhr.status;
8167
- reject(error);
8222
+ const err = new Error(`PUT failed with status ${xhr.status}`);
8223
+ err.status = xhr.status;
8224
+ reject(err);
8168
8225
  }
8169
8226
  });
8170
8227
  xhr.addEventListener("error", () => {
@@ -8173,93 +8230,291 @@ function executeUpload(file, uploadUrl, publicUrl, options) {
8173
8230
  });
8174
8231
  xhr.addEventListener("abort", () => {
8175
8232
  signal?.removeEventListener("abort", handleAbort);
8176
- reject(new DOMException("Upload aborted", "AbortError"));
8233
+ reject(toAbortError("Upload aborted"));
8177
8234
  });
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
8235
+ xhr.open("PUT", url);
8236
+ for (const [k, v] of Object.entries(headers)) {
8237
+ try {
8238
+ xhr.setRequestHeader(k, v);
8239
+ } catch {
8240
+ }
8241
+ }
8242
+ xhr.send(body);
8188
8243
  });
8189
8244
  }
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;
8245
+ function resolveXhrEtag(xhr) {
8246
+ const canonical = xhr.getResponseHeader("ETag");
8247
+ if (canonical !== null) return canonical;
8248
+ return xhr.getResponseHeader("etag") ?? "";
8201
8249
  }
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
8250
+ async function putBlobFetch(url, body, headers, signal, onProgress) {
8251
+ let stream = body;
8252
+ if (onProgress && typeof TransformStream !== "undefined" && typeof body.stream === "function") {
8253
+ let loaded = 0;
8254
+ const counter = new TransformStream({
8255
+ transform(chunk, controller) {
8256
+ loaded += chunk.byteLength;
8257
+ onProgress(loaded);
8258
+ controller.enqueue(chunk);
8259
+ }
8260
+ });
8261
+ stream = body.stream().pipeThrough(counter);
8262
+ }
8263
+ const res = await fetch(url, {
8264
+ method: "PUT",
8265
+ body: stream,
8266
+ headers,
8267
+ signal,
8268
+ // Required when streaming a request body in Chromium / undici.
8269
+ // Cast: TS lib lacks `duplex`.
8270
+ ...{ duplex: "half" }
8206
8271
  });
8272
+ if (!res.ok) {
8273
+ const err = new Error(`PUT failed with status ${res.status}`);
8274
+ err.status = res.status;
8275
+ throw err;
8276
+ }
8277
+ const etag = resolveFetchEtag(res.headers);
8278
+ if (onProgress) onProgress(body.size);
8279
+ return { etag: stripQuotes(etag), status: res.status };
8207
8280
  }
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;
8281
+ function resolveFetchEtag(headers) {
8282
+ const lowerCase = headers.get("etag");
8283
+ if (lowerCase !== null) return lowerCase;
8284
+ return headers.get("ETag") ?? "";
8215
8285
  }
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;
8286
+ function stripQuotes(s) {
8287
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1);
8288
+ return s;
8223
8289
  }
8224
- async function softDeleteFile(config, fileId) {
8225
- await callApi(config, `/storage/files/${encodeURIComponent(fileId)}`, {
8226
- method: "DELETE"
8290
+ async function putWithRetry(url, body, headers, signal, onProgress) {
8291
+ let lastErr;
8292
+ const maxRetries = 5;
8293
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
8294
+ if (signal?.aborted) throw toAbortError();
8295
+ try {
8296
+ return await putBlob(url, body, headers, signal, onProgress);
8297
+ } catch (err) {
8298
+ if (isAbortError(err)) throw err;
8299
+ lastErr = err;
8300
+ if (attempt === maxRetries || !isRetryableError2(err)) throw err;
8301
+ await sleep(calculateBackoffDelay(attempt), signal);
8302
+ }
8303
+ }
8304
+ throw lastErr;
8305
+ }
8306
+ async function uploadsCreate(config, blob, options) {
8307
+ const signal = options.signal;
8308
+ if (signal?.aborted) throw toAbortError();
8309
+ const filename = options.filename ?? (isFile(blob) ? blob.name : void 0) ?? "upload.bin";
8310
+ const contentType = options.contentType ?? (blob.type && blob.type.length > 0 ? blob.type : "application/octet-stream");
8311
+ const size = blob.size;
8312
+ const checksumSha256 = options.checksumSha256 ?? await computeSha256Hex(blob);
8313
+ const idempotencyKey = options.idempotencyKey ?? uuidv7();
8314
+ const createBody = {
8315
+ filename,
8316
+ contentType,
8317
+ size,
8318
+ folder: options.folder,
8319
+ visibility: options.visibility,
8320
+ metadata: options.metadata,
8321
+ checksumSha256,
8322
+ ifNoneMatch: options.ifNoneMatch
8323
+ };
8324
+ const session = await callApi(config, PATHS.uploads, {
8325
+ method: "POST",
8326
+ body: createBody,
8327
+ idempotencyKey,
8328
+ signal
8227
8329
  });
8330
+ const uploadId = session.uploadId;
8331
+ const onAborted = async () => {
8332
+ try {
8333
+ await callApi(config, PATHS.upload(uploadId), { method: "DELETE" });
8334
+ } catch {
8335
+ }
8336
+ clearResume(String(uploadId));
8337
+ };
8338
+ try {
8339
+ if (session.method === "PUT") {
8340
+ return await runSinglePart(config, blob, session, options, idempotencyKey);
8341
+ }
8342
+ return await runMultipart(config, blob, session, options, idempotencyKey);
8343
+ } catch (err) {
8344
+ if (isAbortError(err)) {
8345
+ await onAborted();
8346
+ }
8347
+ throw err;
8348
+ }
8228
8349
  }
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;
8350
+ function isFile(b) {
8351
+ return typeof b.name === "string";
8236
8352
  }
8237
- async function downloadFileVersion(config, fileId, versionId) {
8238
- const signed = await callApi(
8353
+ async function runSinglePart(config, blob, session, options, idempotencyKey) {
8354
+ const { onProgress, signal } = options;
8355
+ const total = blob.size;
8356
+ const trackProgress = onProgress ? (loaded) => {
8357
+ onProgress({ loaded, total, partsCompleted: 0, partsTotal: 1 });
8358
+ } : void 0;
8359
+ const put = await putWithRetry(session.url, blob, session.headers, signal, trackProgress);
8360
+ if (onProgress) onProgress({ loaded: total, total, partsCompleted: 1, partsTotal: 1 });
8361
+ const completion = await callApi(
8239
8362
  config,
8240
- "/storage/signed-url",
8363
+ PATHS.uploadComplete(session.uploadId),
8241
8364
  {
8242
8365
  method: "POST",
8243
- body: { fileId, versionId }
8366
+ body: { parts: [{ partNumber: 1, etag: put.etag }] },
8367
+ idempotencyKey: `${idempotencyKey}:complete`,
8368
+ signal
8244
8369
  }
8245
8370
  );
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"
8371
+ clearResume(String(session.uploadId));
8372
+ return await fetchFile(config, completion.fileId);
8373
+ }
8374
+ async function runMultipart(config, blob, session, options, idempotencyKey) {
8375
+ const { onProgress, signal } = options;
8376
+ const partSize = session.partSize;
8377
+ const total = blob.size;
8378
+ const partsTotal = session.partCount;
8379
+ const completedParts = [];
8380
+ const partLoaded = /* @__PURE__ */ new Map();
8381
+ const reportProgress = () => {
8382
+ if (!onProgress) return;
8383
+ let loaded = 0;
8384
+ for (const v of partLoaded.values()) loaded += v;
8385
+ onProgress({ loaded, total, partsCompleted: completedParts.length, partsTotal });
8386
+ };
8387
+ for (const part of session.parts) {
8388
+ if (signal?.aborted) throw toAbortError();
8389
+ const offset = (part.partNumber - 1) * partSize;
8390
+ const end = Math.min(offset + partSize, total);
8391
+ const slice = blob.slice(offset, end);
8392
+ const trackProgress = onProgress ? (loaded) => {
8393
+ partLoaded.set(part.partNumber, loaded);
8394
+ reportProgress();
8395
+ } : void 0;
8396
+ const result = await putWithRetry(part.url, slice, {}, signal, trackProgress);
8397
+ completedParts.push({ partNumber: part.partNumber, etag: result.etag });
8398
+ partLoaded.set(part.partNumber, slice.size);
8399
+ reportProgress();
8400
+ persistResume({
8401
+ uploadId: String(session.uploadId),
8402
+ completedParts: [...completedParts],
8403
+ updatedAt: Date.now()
8250
8404
  });
8251
8405
  }
8252
- return res.blob();
8406
+ const completion = await callApi(
8407
+ config,
8408
+ PATHS.uploadComplete(session.uploadId),
8409
+ {
8410
+ method: "POST",
8411
+ body: { parts: completedParts },
8412
+ idempotencyKey: `${idempotencyKey}:complete`,
8413
+ signal
8414
+ }
8415
+ );
8416
+ clearResume(String(session.uploadId));
8417
+ return await fetchFile(config, completion.fileId);
8253
8418
  }
8254
- async function getSignedUrl(config, fileId, options) {
8255
- return callApi(config, "/storage/signed-url", {
8256
- method: "POST",
8257
- body: {
8258
- fileId,
8259
- ...options
8419
+ async function fetchFile(config, fileId) {
8420
+ return callApi(config, PATHS.file(fileId), { method: "GET" });
8421
+ }
8422
+ async function uploadsAbort(config, uploadId) {
8423
+ await withRetry(() => callApi(config, PATHS.upload(uploadId), { method: "DELETE" }), {
8424
+ maxRetries: 3
8425
+ });
8426
+ clearResume(String(uploadId));
8427
+ }
8428
+ function filesListPage(config, options, cursor) {
8429
+ const query = {
8430
+ folder: options.folder,
8431
+ cursor: cursor ?? options.cursor,
8432
+ limit: options.limit,
8433
+ includeDeleted: options.includeDeleted
8434
+ };
8435
+ return callApi(config, PATHS.files, {
8436
+ method: "GET",
8437
+ query: {
8438
+ folder: query.folder,
8439
+ cursor: query.cursor,
8440
+ limit: query.limit,
8441
+ includeDeleted: query.includeDeleted
8260
8442
  }
8261
8443
  });
8262
8444
  }
8445
+ function filesList(config, options = {}) {
8446
+ const fetchPage = (cursor) => filesListPage(config, options, cursor);
8447
+ const iter = {
8448
+ [Symbol.asyncIterator]: async function* () {
8449
+ let cursor = options.cursor;
8450
+ do {
8451
+ const page2 = await fetchPage(cursor ?? void 0);
8452
+ for (const f of page2.files) yield f;
8453
+ cursor = page2.nextCursor;
8454
+ } while (cursor);
8455
+ }
8456
+ };
8457
+ return Object.assign(iter, { fetchPage });
8458
+ }
8459
+ async function filesGet(config, fileId) {
8460
+ return callApi(config, PATHS.file(fileId), { method: "GET" });
8461
+ }
8462
+ async function filesDelete(config, fileId) {
8463
+ return callApi(config, PATHS.file(fileId), { method: "DELETE" });
8464
+ }
8465
+ async function filesRestore(config, fileId) {
8466
+ return callApi(config, PATHS.fileRestore(fileId), { method: "POST" });
8467
+ }
8468
+ async function filesSignedUrl(config, fileId, options = {}) {
8469
+ const body = {
8470
+ expiresIn: options.expiresIn,
8471
+ disposition: options.disposition,
8472
+ userId: options.userId
8473
+ };
8474
+ return callApi(config, PATHS.fileSignedUrl(fileId), {
8475
+ method: "POST",
8476
+ body
8477
+ });
8478
+ }
8479
+ async function filesCopy(config, fileId, options) {
8480
+ const body = {
8481
+ folder: options.folder,
8482
+ filename: options.filename,
8483
+ visibility: options.visibility,
8484
+ metadata: options.metadata
8485
+ };
8486
+ return callApi(config, PATHS.fileCopy(fileId), {
8487
+ method: "POST",
8488
+ body
8489
+ });
8490
+ }
8491
+ async function filesVersionsList(config, fileId) {
8492
+ const data = await callApi(config, PATHS.versions(fileId), {
8493
+ method: "GET"
8494
+ });
8495
+ return data.versions;
8496
+ }
8497
+ async function filesVersionsRestore(config, fileId, versionId) {
8498
+ return callApi(config, PATHS.versionRestore(fileId, versionId), { method: "POST" });
8499
+ }
8500
+ var storage = {
8501
+ uploads: {
8502
+ create: uploadsCreate,
8503
+ abort: uploadsAbort
8504
+ },
8505
+ files: {
8506
+ list: filesList,
8507
+ get: filesGet,
8508
+ delete: filesDelete,
8509
+ restore: filesRestore,
8510
+ signedUrl: filesSignedUrl,
8511
+ copy: filesCopy,
8512
+ versions: {
8513
+ list: filesVersionsList,
8514
+ restore: filesVersionsRestore
8515
+ }
8516
+ }
8517
+ };
8263
8518
 
8264
8519
  // src/lib/notifications/service-worker.ts
8265
8520
  function initPushServiceWorker(config = {}) {
@@ -8648,7 +8903,7 @@ function createTasksHandler(taskDefs, options = {}) {
8648
8903
  { status: 400 }
8649
8904
  );
8650
8905
  }
8651
- const signingSecret = options.signingSecret ?? process.env.SYLPHX_SIGNING_SECRET ?? process.env.SYLPHX_SECRET_KEY ?? "";
8906
+ const signingSecret = options.signingSecret ?? process.env.SYLPHX_SIGNING_SECRET ?? "";
8652
8907
  if (signingSecret) {
8653
8908
  const signature = req.headers.get("x-sylphx-signature") ?? "";
8654
8909
  if (!signature) {
@@ -10706,7 +10961,6 @@ export {
10706
10961
  deleteCron,
10707
10962
  deleteDocument,
10708
10963
  deleteEnvVar,
10709
- deleteFile,
10710
10964
  deleteOrganization,
10711
10965
  deletePasskey,
10712
10966
  deletePermission,
@@ -10718,7 +10972,6 @@ export {
10718
10972
  disableDebug,
10719
10973
  disableTwoFactor,
10720
10974
  disconnectOAuthProvider,
10721
- downloadFileVersion,
10722
10975
  dpop,
10723
10976
  embed,
10724
10977
  enableDebug,
@@ -10751,14 +11004,13 @@ export {
10751
11004
  getErrorCode,
10752
11005
  getErrorMessage,
10753
11006
  getFacets,
10754
- getFileInfo,
10755
- getFileUrl,
10756
11007
  getFlagPayload,
10757
11008
  getFlags,
10758
11009
  getLeaderboard,
10759
11010
  getMemberPermissions,
10760
11011
  getMyReferralCode,
10761
11012
  getOidcDiscoveryDocument,
11013
+ getOrgScopedToken,
10762
11014
  getOrganization,
10763
11015
  getOrganizationInvitations,
10764
11016
  getOrganizationMembers,
@@ -10779,7 +11031,6 @@ export {
10779
11031
  getSecrets,
10780
11032
  getSecurityScore,
10781
11033
  getSession,
10782
- getSignedUrl,
10783
11034
  getStreak,
10784
11035
  getSubscription,
10785
11036
  getTask,
@@ -10837,7 +11088,6 @@ export {
10837
11088
  leaveOrganization,
10838
11089
  linkAnonymousConsents,
10839
11090
  listEnvVars,
10840
- listFileVersions,
10841
11091
  listOAuthProviders,
10842
11092
  listPasskeys,
10843
11093
  listPermissions,
@@ -10885,8 +11135,6 @@ export {
10885
11135
  resetPassword,
10886
11136
  resetPlatformCookieCache,
10887
11137
  resetPlatformJwksCache,
10888
- restoreFile,
10889
- restoreFileVersion,
10890
11138
  resumeCron,
10891
11139
  revokeAllTokens,
10892
11140
  revokeOrganizationInvitation,
@@ -10908,8 +11156,8 @@ export {
10908
11156
  signIn,
10909
11157
  signOut,
10910
11158
  signUp,
10911
- softDeleteFile,
10912
11159
  startPasskeyRegistration,
11160
+ storage,
10913
11161
  streamToString,
10914
11162
  submitScore,
10915
11163
  suspendUser,
@@ -10930,8 +11178,6 @@ export {
10930
11178
  updateUserMetadata,
10931
11179
  updateUserProfile,
10932
11180
  updateWebhookConfig,
10933
- uploadAvatar,
10934
- uploadFile,
10935
11181
  upsertDocument,
10936
11182
  user,
10937
11183
  userInfo,