@omelhorsite/sdk 0.4.4 → 0.5.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/README.md CHANGED
@@ -163,9 +163,13 @@ const nodes = await oms.storage.upload(
163
163
  );
164
164
  ```
165
165
 
166
- Progress arrives per finished file or part, never per byte. A file rejected on
167
- its own (quota, name collision) does not throw; it is missing from the
168
- returned array, so compare lengths when partial success matters.
166
+ In a browser or on React Native, progress is reported byte by byte (the bytes
167
+ travel through `XMLHttpRequest`); elsewhere it arrives per finished file or
168
+ part. The same goes for every other upload: pass `onUploadProgress` in the
169
+ options of `tools.*.create`, `library.books.create` or
170
+ `chests.entries.createWithUpload`. A file rejected on its own (quota, name
171
+ collision) does not throw; it is missing from the returned array, so compare
172
+ lengths when partial success matters.
169
173
 
170
174
  ## Long jobs: `create` / `get`, and `run`
171
175
 
package/dist/index.js CHANGED
@@ -344,6 +344,87 @@ async function collect(first, limit = Number.POSITIVE_INFINITY) {
344
344
  return out;
345
345
  }
346
346
 
347
+ // src/internal/xhr.ts
348
+ function xhrAvailable() {
349
+ return typeof XMLHttpRequest === "function";
350
+ }
351
+ function xhrFetch(onUploadProgress) {
352
+ return (input, init) => new Promise((resolve, reject) => {
353
+ const xhr = new XMLHttpRequest;
354
+ xhr.open(init?.method ?? "GET", input, true);
355
+ xhr.responseType = "arraybuffer";
356
+ xhr.withCredentials = init?.credentials === "include";
357
+ applyHeaders(xhr, init?.headers);
358
+ const signal = init?.signal ?? undefined;
359
+ const onAbort = () => xhr.abort();
360
+ if (signal) {
361
+ if (signal.aborted) {
362
+ reject(abortError());
363
+ return;
364
+ }
365
+ signal.addEventListener("abort", onAbort, { once: true });
366
+ }
367
+ const settle = () => signal?.removeEventListener("abort", onAbort);
368
+ xhr.upload.onprogress = (event) => {
369
+ onUploadProgress({
370
+ phase: "upload",
371
+ loaded: event.loaded,
372
+ total: event.lengthComputable ? event.total : undefined
373
+ });
374
+ };
375
+ xhr.onload = () => {
376
+ settle();
377
+ if (xhr.status === 0) {
378
+ reject(new TypeError("Network request failed"));
379
+ return;
380
+ }
381
+ resolve(toResponse(xhr));
382
+ };
383
+ xhr.onerror = () => {
384
+ settle();
385
+ reject(new TypeError("Network request failed"));
386
+ };
387
+ xhr.onabort = () => {
388
+ settle();
389
+ reject(abortError());
390
+ };
391
+ xhr.send(init?.body ?? null);
392
+ });
393
+ }
394
+ function applyHeaders(xhr, headers) {
395
+ if (!headers)
396
+ return;
397
+ if (headers instanceof Headers) {
398
+ headers.forEach((value, key) => xhr.setRequestHeader(key, value));
399
+ } else if (Array.isArray(headers)) {
400
+ for (const [key, value] of headers)
401
+ xhr.setRequestHeader(key, value);
402
+ } else {
403
+ for (const [key, value] of Object.entries(headers))
404
+ xhr.setRequestHeader(key, value);
405
+ }
406
+ }
407
+ function toResponse(xhr) {
408
+ const headers = new Headers;
409
+ for (const line of xhr.getAllResponseHeaders().split(/\r?\n/)) {
410
+ const separator = line.indexOf(":");
411
+ if (separator <= 0)
412
+ continue;
413
+ headers.append(line.slice(0, separator).trim(), line.slice(separator + 1).trim());
414
+ }
415
+ const bodyless = xhr.status === 204 || xhr.status === 205 || xhr.status === 304;
416
+ return new Response(bodyless ? null : xhr.response, {
417
+ status: xhr.status,
418
+ statusText: xhr.statusText,
419
+ headers
420
+ });
421
+ }
422
+ function abortError() {
423
+ const error = new Error("The operation was aborted.");
424
+ error.name = "AbortError";
425
+ return error;
426
+ }
427
+
347
428
  // src/http.ts
348
429
  var DEFAULT_BASE_URL = "https://backend.omelhorsite.pt";
349
430
  function transportCapabilities() {
@@ -382,6 +463,7 @@ function supportsNativeFormDataFiles() {
382
463
  class ApiClient {
383
464
  baseUrl;
384
465
  fetchImpl;
466
+ customFetch;
385
467
  tokens;
386
468
  sessionCookie;
387
469
  baseHeaders;
@@ -394,6 +476,7 @@ class ApiClient {
394
476
  throw new OmsNetworkError("No fetch implementation available. Pass one to the Oms constructor: new Oms({ fetch }).");
395
477
  }
396
478
  this.fetchImpl = (input, init) => injected(input, init);
479
+ this.customFetch = options.fetch !== undefined;
397
480
  if (options.sessionCookie && options.tokens) {
398
481
  throw new TypeError("Pass either `sessionCookie` or a token, not both: two credentials on one request means the server decides which identity wins, and the caller cannot tell which one it got.");
399
482
  }
@@ -471,6 +554,15 @@ class ApiClient {
471
554
  size: blob.size
472
555
  };
473
556
  }
557
+ transportFor(onUploadProgress) {
558
+ if (onUploadProgress && !this.customFetch && xhrAvailable())
559
+ return xhrFetch(onUploadProgress);
560
+ return this.fetchImpl;
561
+ }
562
+ async patchForm(path, fields, options = {}) {
563
+ const form = await buildFormData(fields);
564
+ return this.requestJson("PATCH", path, { ...options, body: form, isFormData: true });
565
+ }
474
566
  url(path, query) {
475
567
  const suffix = path.startsWith("/") ? path : `/${path}`;
476
568
  const encoded = query ? encodeQuery(query) : "";
@@ -493,7 +585,7 @@ class ApiClient {
493
585
  const deadline = createDeadline(timeoutMs, options.signal);
494
586
  let response;
495
587
  try {
496
- response = await this.fetchImpl(url, await this.buildInit(method, options, deadline.signal));
588
+ response = await this.transportFor(options.onUploadProgress)(url, await this.buildInit(method, options, deadline.signal));
497
589
  } catch (thrown) {
498
590
  deadline.dispose();
499
591
  const failure = classifyFetchFailure(thrown, { method, url, attempts: attempt, timeoutMs, options });
@@ -9532,11 +9624,13 @@ class StorageRateGate {
9532
9624
  class UploadManager extends Resource {
9533
9625
  gate;
9534
9626
  transport;
9627
+ ownTransport;
9535
9628
  md5;
9536
9629
  constructor(http, options = {}) {
9537
9630
  super(http);
9538
9631
  this.gate = new StorageRateGate(FS_UPLOAD_RATE_LIMIT);
9539
9632
  this.transport = options.fetch ?? objectStoreFetch(http);
9633
+ this.ownTransport = options.fetch === undefined;
9540
9634
  this.md5 = options.md5 ?? md5Base64;
9541
9635
  }
9542
9636
  async upload(input, options = {}) {
@@ -9557,12 +9651,23 @@ class UploadManager extends Resource {
9557
9651
  };
9558
9652
  }));
9559
9653
  const total = prepared.reduce((sum, entry) => sum + entry.blob.size, 0);
9560
- let loaded = 0;
9561
- const report = (delta) => {
9562
- loaded += delta;
9563
- options.onProgress?.({ phase: "upload", loaded, total });
9654
+ let completed = 0;
9655
+ const inFlight = new Map;
9656
+ const emit = () => {
9657
+ let extra = 0;
9658
+ inFlight.forEach((bytes) => extra += bytes);
9659
+ options.onProgress?.({ phase: "upload", loaded: completed + extra, total });
9660
+ };
9661
+ const fileProgress = (clientId, size) => (progress) => {
9662
+ inFlight.set(clientId, Math.min(progress.loaded, size));
9663
+ emit();
9564
9664
  };
9565
- report(0);
9665
+ const fileDone = (clientId, size) => {
9666
+ inFlight.delete(clientId);
9667
+ completed += size;
9668
+ emit();
9669
+ };
9670
+ emit();
9566
9671
  const { onProgress: _runProgress, ...transfer } = options;
9567
9672
  const batchSize = Math.min(MAX_BATCH, Math.max(1, Math.trunc(input.batchSize ?? DEFAULT_BATCH_SIZE)));
9568
9673
  const concurrency = Math.max(1, Math.trunc(input.concurrency ?? DEFAULT_UPLOAD_CONCURRENCY));
@@ -9593,19 +9698,24 @@ class UploadManager extends Resource {
9593
9698
  if (plan.strategy === "multipart") {
9594
9699
  await this.uploadMultipart(plan.fs_node_id, entry.blob, {
9595
9700
  ...transfer,
9596
- onPart: (bytes) => report(bytes),
9701
+ onProgress: fileProgress(entry.clientId, entry.blob.size),
9597
9702
  onSession: (token) => open.set(plan.fs_node_id, token)
9598
9703
  });
9599
9704
  open.delete(plan.fs_node_id);
9705
+ fileDone(entry.clientId, entry.blob.size);
9600
9706
  } else {
9601
9707
  if (!plan.upload) {
9602
9708
  throw new OmsError(`The server planned a direct upload for "${entry.filename}" but sent no presigned URL.`, "api_error");
9603
9709
  }
9604
- await this.putDirect(plan.upload, entry.blob, transfer);
9710
+ await this.putDirect(plan.upload, entry.blob, {
9711
+ ...transfer,
9712
+ onProgress: fileProgress(entry.clientId, entry.blob.size)
9713
+ });
9605
9714
  attachments.push({ fs_node_id: plan.fs_node_id, blob_signed_id: plan.upload.blob_signed_id });
9606
- report(entry.blob.size);
9715
+ fileDone(entry.clientId, entry.blob.size);
9607
9716
  }
9608
9717
  } catch (thrown) {
9718
+ inFlight.delete(entry.clientId);
9609
9719
  failed.set(entry.clientId, {
9610
9720
  client_id: entry.clientId,
9611
9721
  code: thrown instanceof OmsError ? thrown.code : "transfer_failed",
@@ -9666,8 +9776,18 @@ class UploadManager extends Resource {
9666
9776
  };
9667
9777
  }
9668
9778
  async putDirect(target, body, options = {}) {
9669
- const etag = await this.putBytes(target.url, target.headers, body, options);
9670
- options.onProgress?.({ phase: "upload", loaded: body.size, total: body.size });
9779
+ const { onProgress, ...request } = options;
9780
+ const size = body.size;
9781
+ const etag = await this.putBytes(target.url, target.headers, body, {
9782
+ ...request,
9783
+ ...onProgress ? {
9784
+ onUploadProgress: (progress) => {
9785
+ if (progress.loaded < size)
9786
+ onProgress({ phase: "upload", loaded: progress.loaded, total: size });
9787
+ }
9788
+ } : {}
9789
+ });
9790
+ onProgress?.({ phase: "upload", loaded: size, total: size });
9671
9791
  return etag;
9672
9792
  }
9673
9793
  async attachBlobs(attachments, options = {}) {
@@ -9728,7 +9848,13 @@ class UploadManager extends Resource {
9728
9848
  let loaded = 0;
9729
9849
  for (const partNumber of done.keys())
9730
9850
  loaded += bytesOfPart(partNumber);
9731
- options.onProgress?.({ phase: "upload", loaded, total: body.size });
9851
+ const inFlight = new Map;
9852
+ const report = () => {
9853
+ let extra = 0;
9854
+ inFlight.forEach((bytes) => extra += bytes);
9855
+ options.onProgress?.({ phase: "upload", loaded: loaded + extra, total: body.size });
9856
+ };
9857
+ report();
9732
9858
  if (partCount > MAX_PARTS) {
9733
9859
  throw new OmsError(`storage: ${body.size} bytes needs ${partCount} parts of ${partSize}, over the server's limit of ${MAX_PARTS}.`, "invalid_request");
9734
9860
  }
@@ -9749,7 +9875,14 @@ class UploadManager extends Resource {
9749
9875
  throw new OmsError(`storage: the server presigned no URL for part ${partNumber}.`, "api_error");
9750
9876
  const from = (partNumber - 1) * partSize;
9751
9877
  const chunk = body.slice(from, Math.min(from + partSize, body.size));
9752
- const etag = await this.putBytes(url, {}, chunk, options);
9878
+ const etag = await this.putBytes(url, {}, chunk, {
9879
+ ...options,
9880
+ onUploadProgress: (progress) => {
9881
+ inFlight.set(partNumber, Math.min(progress.loaded, Math.max(0, chunk.size - 1)));
9882
+ report();
9883
+ }
9884
+ });
9885
+ inFlight.delete(partNumber);
9753
9886
  if (!etag) {
9754
9887
  throw new OmsError("storage: the object store did not expose the part ETag. In a browser this means the bucket's CORS policy is missing ETag in Access-Control-Expose-Headers.", "unsupported");
9755
9888
  }
@@ -9757,7 +9890,7 @@ class UploadManager extends Resource {
9757
9890
  done.set(partNumber, part);
9758
9891
  options.onPart?.(chunk.size, part);
9759
9892
  loaded += chunk.size;
9760
- options.onProgress?.({ phase: "upload", loaded, total: body.size });
9893
+ report();
9761
9894
  });
9762
9895
  }
9763
9896
  return this.multipartComplete(fsNodeId, { uploadToken, parts: [...done.values()] }, options);
@@ -9765,10 +9898,11 @@ class UploadManager extends Resource {
9765
9898
  async putBytes(url, headers, body, options) {
9766
9899
  const retry = options.retry === false ? false : resolveRetry(options.retry ?? { maxAttempts: 4 });
9767
9900
  const maxAttempts = retry === false ? 1 : Math.max(1, retry.maxAttempts);
9901
+ const transport = this.ownTransport && options.onUploadProgress ? this.http.transportFor(options.onUploadProgress) : this.transport;
9768
9902
  for (let attempt = 1;; attempt += 1) {
9769
9903
  let response;
9770
9904
  try {
9771
- response = await this.transport(url, {
9905
+ response = await transport(url, {
9772
9906
  method: "PUT",
9773
9907
  headers,
9774
9908
  body,
@@ -10401,6 +10535,41 @@ class AdminShortLinksNamespace extends Resource {
10401
10535
  }
10402
10536
  }
10403
10537
 
10538
+ // src/resources/admin/users.ts
10539
+ class AdminUsersNamespace extends Resource {
10540
+ async update(id, input, options = {}) {
10541
+ const path = `/users/${encodeURIComponent(id)}`;
10542
+ const body = adminUpdateBody(input);
10543
+ if (input.picture === undefined)
10544
+ return this.http.patch(path, body, options);
10545
+ return this.http.patchForm(path, { ...body, picture: input.picture }, options);
10546
+ }
10547
+ }
10548
+ function adminUpdateBody(input) {
10549
+ const body = {};
10550
+ const set = (key, value) => {
10551
+ if (value !== undefined)
10552
+ body[key] = value;
10553
+ };
10554
+ set("handle", input.handle);
10555
+ set("name", input.name);
10556
+ set("bio", input.bio);
10557
+ set("country_code", input.countryCode);
10558
+ set("email_is_public", input.emailIsPublic);
10559
+ set("gender_is_public", input.genderIsPublic);
10560
+ set("gender", input.gender);
10561
+ set("library_public", input.libraryPublic);
10562
+ set("library_name", input.libraryName);
10563
+ set("library_description", input.libraryDescription);
10564
+ set("share_listening", input.shareListening);
10565
+ set("email", input.email);
10566
+ set("group", input.group);
10567
+ if (input.password)
10568
+ set("password", input.password);
10569
+ set("allowed_to_use_spotify", input.allowedToUseSpotify);
10570
+ return body;
10571
+ }
10572
+
10404
10573
  // src/resources/admin/vocalSeparations.ts
10405
10574
  var ADMIN_VOCAL_SEPARATION_FILTER_COLUMNS = Object.freeze([
10406
10575
  "id",
@@ -10486,6 +10655,7 @@ class AdminNamespace extends Resource {
10486
10655
  chests;
10487
10656
  notepads;
10488
10657
  eventAlerts;
10658
+ users;
10489
10659
  constructor(http) {
10490
10660
  super(http);
10491
10661
  this.myApplications = new MyOauthApplicationsNamespace(http);
@@ -10499,6 +10669,7 @@ class AdminNamespace extends Resource {
10499
10669
  this.chests = new AdminChestsNamespace(http);
10500
10670
  this.notepads = new AdminNotepadsNamespace(http);
10501
10671
  this.eventAlerts = new AdminEventAlertsNamespace(http);
10672
+ this.users = new AdminUsersNamespace(http);
10502
10673
  }
10503
10674
  }
10504
10675
 
@@ -10773,13 +10944,15 @@ class ChestEntriesNamespace extends Resource {
10773
10944
  async createWithUpload(input, options = {}) {
10774
10945
  const { blob, filename } = await readFileInput(input.file);
10775
10946
  const name = input.name ?? filename;
10947
+ const { onReserved, ...operation } = options;
10776
10948
  const entry = await this.http.post("/chest_entries", {
10777
10949
  chest_id: input.chestId,
10778
10950
  kind: "file",
10779
10951
  name,
10780
10952
  size: blob.size,
10781
10953
  chest_token: input.chestToken
10782
- }, { retry: false, ...options });
10954
+ }, { retry: false, ...operation });
10955
+ onReserved?.(entry);
10783
10956
  try {
10784
10957
  const checksum = await md5Base64(blob);
10785
10958
  const { signed_url: target } = await this.http.post(`/chest_entries/${encodeURIComponent(entry.id)}/attachment_signed_url`, { checksum }, { retry: false, ...options });
@@ -14845,14 +15018,27 @@ class CaptionsNamespace extends Resource {
14845
15018
  for (const offset of offsets)
14846
15019
  if (done.has(offset))
14847
15020
  loaded += Math.min(partSize, total - offset);
14848
- onProgress?.({ phase: "upload", loaded, total });
15021
+ const inFlight = new Map;
15022
+ const report = () => {
15023
+ let extra = 0;
15024
+ inFlight.forEach((bytes) => extra += bytes);
15025
+ onProgress?.({ phase: "upload", loaded: loaded + extra, total });
15026
+ };
15027
+ report();
14849
15028
  const concurrency = Math.max(1, Math.trunc(input.concurrency ?? CAPTION_UPLOAD_CONCURRENCY));
14850
15029
  await runCaptionParts(pending, concurrency, async (offset) => {
14851
15030
  const chunk = blob.slice(offset, Math.min(offset + partSize, total));
14852
- await this.uploadPart(session, offset, chunk, request);
15031
+ await this.uploadPart(session, offset, chunk, {
15032
+ ...request,
15033
+ onUploadProgress: (progress) => {
15034
+ inFlight.set(offset, Math.min(progress.loaded, Math.max(0, chunk.size - 1)));
15035
+ report();
15036
+ }
15037
+ });
15038
+ inFlight.delete(offset);
14853
15039
  onPart?.(offset, chunk.size);
14854
15040
  loaded += chunk.size;
14855
- onProgress?.({ phase: "upload", loaded, total });
15041
+ report();
14856
15042
  });
14857
15043
  return this.finishUpload(session, request);
14858
15044
  }
@@ -15819,6 +16005,7 @@ export {
15819
16005
  ApiClient,
15820
16006
  AnalysisNamespace,
15821
16007
  AdminVocalSeparationsNamespace,
16008
+ AdminUsersNamespace,
15822
16009
  AdminShortLinksNamespace,
15823
16010
  AdminQuotasNamespace,
15824
16011
  AdminOauthApplicationsNamespace,
@@ -23,7 +23,7 @@
23
23
  * polyfill after the SDK is imported, and in RN the fetch implementation is
24
24
  * swapped by libraries often enough that a cached answer goes stale.
25
25
  */
26
- import { type FetchLike, type FileInput, type FileOutput, type NativeFile, type QueryParams, type RequestOptions, type ResolvedRetry, type RetryOptions } from "./types";
26
+ import { type FetchLike, type FileInput, type FileOutput, type NativeFile, type ProgressCallback, type QueryParams, type RequestOptions, type ResolvedRetry, type RetryOptions } from "./types";
27
27
  /** Production API root. Override only for a local backend or a test double. */
28
28
  export declare const DEFAULT_BASE_URL = "https://backend.omelhorsite.pt";
29
29
  /**
@@ -94,11 +94,11 @@ export declare function supportsResponseStreaming(): boolean;
94
94
  * True in a browser and in React Native (RN's networking is XHR underneath and
95
95
  * `xhr.upload.onprogress` fires there); false in a Worker-class isolate and in
96
96
  * Bun's server runtime. See {@link Progress} for the whole argument and
97
- * `UploadManagerOptions.fetch` for the recipe - this only reports whether that
98
- * recipe can be used here, so a UI can decide between a real bar and a
99
- * per-file tick.
97
+ * `RequestOptions.onUploadProgress`; this reports whether that can work here,
98
+ * so a UI can decide between a real bar and a per-file tick.
100
99
  *
101
- * The SDK never uses XHR itself. It cannot: the core has to load in an isolate.
100
+ * A request that asks for `onUploadProgress` travels through it when it is
101
+ * present; nothing else in the SDK touches it.
102
102
  */
103
103
  export declare function supportsUploadProgress(): boolean;
104
104
  /**
@@ -274,6 +274,7 @@ export declare class ApiClient {
274
274
  /** API root with no trailing slash. */
275
275
  readonly baseUrl: string;
276
276
  private readonly fetchImpl;
277
+ private readonly customFetch;
277
278
  private readonly tokens;
278
279
  private readonly sessionCookie;
279
280
  private readonly baseHeaders;
@@ -383,6 +384,14 @@ export declare class ApiClient {
383
384
  * downloader instead of pulling it through JavaScript.
384
385
  */
385
386
  download(path: string, options?: GetOptions): Promise<FileOutput>;
387
+ /**
388
+ * The transport a request should travel on: the client's fetch, or an
389
+ * `XMLHttpRequest` when upload progress was asked for and the runtime has
390
+ * one. A custom `fetch` is never bypassed.
391
+ */
392
+ transportFor(onUploadProgress?: ProgressCallback): FetchLike;
393
+ /** `PATCH` with a multipart body, parsed as JSON. */
394
+ patchForm<T>(path: string, fields: FormFields, options?: GetOptions): Promise<T>;
386
395
  /** Absolute URL for a path, with the query string applied. */
387
396
  url(path: string, query?: QueryParams): string;
388
397
  private requestJson;
@@ -0,0 +1,6 @@
1
+ /** A fetch-shaped XMLHttpRequest, for the one thing fetch cannot do: report upload progress. */
2
+ import type { FetchLike, ProgressCallback } from "../types";
3
+ /** Whether this runtime can send a request through `XMLHttpRequest`. */
4
+ export declare function xhrAvailable(): boolean;
5
+ /** A {@link FetchLike} over `XMLHttpRequest` that reports the bytes sent to `onUploadProgress`. */
6
+ export declare function xhrFetch(onUploadProgress: ProgressCallback): FetchLike;
@@ -78,14 +78,17 @@ export interface UserSearchResult {
78
78
  export interface UpdateAccountInput {
79
79
  readonly handle?: string;
80
80
  readonly name?: string;
81
- readonly bio?: string;
81
+ /** `null` clears it. */
82
+ readonly bio?: string | null;
82
83
  readonly countryCode?: string;
83
84
  readonly emailIsPublic?: boolean;
84
85
  readonly genderIsPublic?: boolean;
85
86
  readonly gender?: string;
86
87
  readonly libraryPublic?: boolean;
87
- readonly libraryName?: string;
88
- readonly libraryDescription?: string;
88
+ /** `null` clears it. */
89
+ readonly libraryName?: string | null;
90
+ /** `null` clears it. */
91
+ readonly libraryDescription?: string | null;
89
92
  /** Whether friends may see what you are listening to. */
90
93
  readonly shareListening?: boolean;
91
94
  }
@@ -10,6 +10,7 @@ import { AdminNotepadsNamespace } from "./notepads";
10
10
  import { AdminOauthApplicationsNamespace } from "./oauthApplications";
11
11
  import { AdminQuotasNamespace } from "./quotas";
12
12
  import { AdminShortLinksNamespace } from "./shortLinks";
13
+ import { AdminUsersNamespace } from "./users";
13
14
  import { AdminVocalSeparationsNamespace } from "./vocalSeparations";
14
15
  export * from "./authorizedApplications";
15
16
  export * from "./chests";
@@ -22,6 +23,7 @@ export * from "./oauthApplications";
22
23
  export * from "./quotas";
23
24
  export * from "./shortLinks";
24
25
  export * from "./types";
26
+ export * from "./users";
25
27
  export * from "./vocalSeparations";
26
28
  /**
27
29
  * The `admin` namespace, reachable as `oms.admin`.
@@ -73,5 +75,7 @@ export declare class AdminNamespace extends Resource {
73
75
  readonly notepads: AdminNotepadsNamespace;
74
76
  /** The Discord alert catalogue. **Administrators only.** */
75
77
  readonly eventAlerts: AdminEventAlertsNamespace;
78
+ /** Any account, admin-only fields included. */
79
+ readonly users: AdminUsersNamespace;
76
80
  constructor(http: ConstructorParameters<typeof Resource>[0]);
77
81
  }
@@ -0,0 +1,41 @@
1
+ /** `oms.admin.users`: editing any user's account. Administrators only. */
2
+ import { Resource } from "../../http";
3
+ import type { FileInput, Id, NativeFile, RequestOptions } from "../../types";
4
+ import type { User } from "../account";
5
+ /**
6
+ * Fields an administrator may change on any account. Everything is optional;
7
+ * omitted fields are left alone, `null` clears the nullable ones.
8
+ *
9
+ * `password` is only written when given and non-empty. `picture` makes the
10
+ * request multipart; the image is re-encoded server-side and capped at 1024px.
11
+ */
12
+ export interface AdminUpdateUserInput {
13
+ readonly handle?: string;
14
+ readonly name?: string;
15
+ readonly bio?: string | null;
16
+ readonly countryCode?: string;
17
+ readonly emailIsPublic?: boolean;
18
+ readonly genderIsPublic?: boolean;
19
+ readonly gender?: string;
20
+ readonly libraryPublic?: boolean;
21
+ readonly libraryName?: string | null;
22
+ readonly libraryDescription?: string | null;
23
+ readonly shareListening?: boolean;
24
+ readonly email?: string;
25
+ /** Privilege group, e.g. `"administrator"`. */
26
+ readonly group?: string;
27
+ readonly password?: string;
28
+ readonly allowedToUseSpotify?: boolean;
29
+ readonly picture?: FileInput | NativeFile;
30
+ }
31
+ export declare class AdminUsersNamespace extends Resource {
32
+ /**
33
+ * `PATCH /users/:id` - edits any account, admin-only fields included.
34
+ *
35
+ * Answers the updated {@link User} as an administrator sees it.
36
+ *
37
+ * @throws {OmsAuthError} 401 for a non-administrator editing somebody else.
38
+ * @throws {OmsApiError} 400 with the validation messages.
39
+ */
40
+ update(id: Id, input: AdminUpdateUserInput, options?: RequestOptions): Promise<User>;
41
+ }
@@ -137,7 +137,9 @@ export declare class ChestEntriesNamespace extends Resource {
137
137
  * The file is buffered whole: the MD5 the presigned signature covers has to
138
138
  * be computed over all of it.
139
139
  */
140
- createWithUpload(input: CreateChestFileInput, options?: OperationOptions): Promise<ChestEntry>;
140
+ createWithUpload(input: CreateChestFileInput, options?: OperationOptions & {
141
+ readonly onReserved?: (entry: ChestEntry) => void;
142
+ }): Promise<ChestEntry>;
141
143
  /**
142
144
  * `GET /chest_entries/:id/data` - the entry's bytes.
143
145
  *
@@ -345,6 +345,7 @@ export declare class UploadManager extends Resource {
345
345
  /** Paces every control-plane call against the 300-a-minute upload limit. */
346
346
  readonly gate: StorageRateGate;
347
347
  private readonly transport;
348
+ private readonly ownTransport;
348
349
  private readonly md5;
349
350
  constructor(http: ApiClient, options?: UploadManagerOptions);
350
351
  /**
@@ -231,6 +231,15 @@ export interface RequestOptions {
231
231
  readonly timeoutMs?: number;
232
232
  /** Extra request headers. Merged over the client's, under `Authorization`. */
233
233
  readonly headers?: Record<string, string>;
234
+ /**
235
+ * Called with the bytes sent so far while the request body goes up.
236
+ *
237
+ * Honoured where `XMLHttpRequest` exists (browsers, React Native) and the
238
+ * client was built without a custom `fetch`, which is when the request is
239
+ * sent through it instead. Elsewhere the request goes out as usual and this
240
+ * is never called.
241
+ */
242
+ readonly onUploadProgress?: ProgressCallback;
234
243
  /**
235
244
  * Per-call retry override, and the ONLY way to put a mutating request in
236
245
  * scope for a retry.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omelhorsite/sdk",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "TypeScript SDK for the omelhorsite API. Isolate-safe: no node builtins, no environment access, no stdout.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",