@omelhorsite/sdk 0.2.0 → 0.4.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.
Files changed (96) hide show
  1. package/README.md +112 -130
  2. package/dist/index.js +4800 -610
  3. package/dist/types/client.d.ts +60 -3
  4. package/dist/types/http.d.ts +444 -19
  5. package/dist/types/index.d.ts +5 -1
  6. package/dist/types/internal/attachments.d.ts +17 -0
  7. package/dist/types/internal/helpers.d.ts +9 -0
  8. package/dist/types/listing.d.ts +90 -0
  9. package/dist/types/resources/account.d.ts +71 -5
  10. package/dist/types/resources/admin/authorizedApplications.d.ts +133 -0
  11. package/dist/types/resources/admin/chests.d.ts +51 -0
  12. package/dist/types/resources/admin/eventAlerts.d.ts +60 -0
  13. package/dist/types/resources/admin/identities.d.ts +99 -0
  14. package/dist/types/resources/admin/index.d.ts +77 -0
  15. package/dist/types/resources/admin/jobs.d.ts +97 -0
  16. package/dist/types/resources/admin/myOauthApplications.d.ts +282 -0
  17. package/dist/types/resources/admin/notepads.d.ts +49 -0
  18. package/dist/types/resources/admin/oauthApplications.d.ts +309 -0
  19. package/dist/types/resources/admin/quotas.d.ts +114 -0
  20. package/dist/types/resources/admin/shortLinks.d.ts +201 -0
  21. package/dist/types/resources/admin/types.d.ts +222 -0
  22. package/dist/types/resources/admin/vocalSeparations.d.ts +98 -0
  23. package/dist/types/resources/admin.d.ts +1841 -0
  24. package/dist/types/resources/auth/index.d.ts +39 -0
  25. package/dist/types/resources/auth/passkeys.d.ts +652 -0
  26. package/dist/types/resources/auth/sessions.d.ts +844 -0
  27. package/dist/types/resources/chests.d.ts +54 -3
  28. package/dist/types/resources/content/analysis.d.ts +62 -0
  29. package/dist/types/resources/content/blogs.d.ts +471 -0
  30. package/dist/types/resources/content/feedbacks.d.ts +263 -0
  31. package/dist/types/resources/content/index.d.ts +52 -0
  32. package/dist/types/resources/content/intel/articles.d.ts +230 -0
  33. package/dist/types/resources/content/intel/config.d.ts +135 -0
  34. package/dist/types/resources/content/intel/index.d.ts +53 -0
  35. package/dist/types/resources/content/intel/items.d.ts +91 -0
  36. package/dist/types/resources/content/intel/reports.d.ts +108 -0
  37. package/dist/types/resources/content/intel/scripts.d.ts +152 -0
  38. package/dist/types/resources/content/intel/sources.d.ts +236 -0
  39. package/dist/types/resources/content/intel/stats.d.ts +105 -0
  40. package/dist/types/resources/content/intel/types.d.ts +86 -0
  41. package/dist/types/resources/content/jokes.d.ts +93 -0
  42. package/dist/types/resources/content/notifications.d.ts +147 -0
  43. package/dist/types/resources/content/serviceUsages.d.ts +73 -0
  44. package/dist/types/resources/content/servicesStatus.d.ts +194 -0
  45. package/dist/types/resources/content/siteConfig.d.ts +43 -0
  46. package/dist/types/resources/content/spaceInvaders.d.ts +154 -0
  47. package/dist/types/resources/content.d.ts +2775 -0
  48. package/dist/types/resources/dynamicQrs.d.ts +39 -3
  49. package/dist/types/resources/forms.d.ts +176 -35
  50. package/dist/types/resources/index.d.ts +19 -8
  51. package/dist/types/resources/ipLookup.d.ts +20 -4
  52. package/dist/types/resources/jobs.d.ts +69 -25
  53. package/dist/types/resources/library/annotations.d.ts +162 -0
  54. package/dist/types/resources/library/books.d.ts +534 -0
  55. package/dist/types/resources/library/chat.d.ts +259 -0
  56. package/dist/types/resources/library/index.d.ts +39 -0
  57. package/dist/types/resources/library/shelves.d.ts +237 -0
  58. package/dist/types/resources/library/types.d.ts +120 -0
  59. package/dist/types/resources/library.d.ts +1433 -0
  60. package/dist/types/resources/linkTrees.d.ts +142 -30
  61. package/dist/types/resources/media.d.ts +351 -0
  62. package/dist/types/resources/movies/addons.d.ts +388 -0
  63. package/dist/types/resources/movies/collections.d.ts +352 -0
  64. package/dist/types/resources/movies/index.d.ts +29 -0
  65. package/dist/types/resources/movies/types.d.ts +50 -0
  66. package/dist/types/resources/movies/watchProgress.d.ts +277 -0
  67. package/dist/types/resources/movies.d.ts +1160 -0
  68. package/dist/types/resources/music/artists.d.ts +1068 -0
  69. package/dist/types/resources/music/imports.d.ts +941 -0
  70. package/dist/types/resources/music/index.d.ts +61 -0
  71. package/dist/types/resources/music/playlists.d.ts +1027 -0
  72. package/dist/types/resources/music/social.d.ts +1132 -0
  73. package/dist/types/resources/music/songs.d.ts +1167 -0
  74. package/dist/types/resources/notepads.d.ts +4 -1
  75. package/dist/types/resources/quotas.d.ts +7 -1
  76. package/dist/types/resources/realtime.d.ts +855 -0
  77. package/dist/types/resources/shortLinks.d.ts +49 -5
  78. package/dist/types/resources/social/groupChats.d.ts +458 -0
  79. package/dist/types/resources/social/index.d.ts +19 -0
  80. package/dist/types/resources/social/messages.d.ts +424 -0
  81. package/dist/types/resources/social/relationships.d.ts +322 -0
  82. package/dist/types/resources/social/types.d.ts +37 -0
  83. package/dist/types/resources/social.d.ts +1348 -0
  84. package/dist/types/resources/storage/upload.d.ts +158 -11
  85. package/dist/types/resources/storage.d.ts +111 -27
  86. package/dist/types/resources/tickets.d.ts +101 -13
  87. package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
  88. package/dist/types/resources/tools/captions.d.ts +448 -21
  89. package/dist/types/resources/tools/downloader.d.ts +21 -0
  90. package/dist/types/resources/tools/index.d.ts +57 -15
  91. package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
  92. package/dist/types/resources/tools/transcription.d.ts +35 -13
  93. package/dist/types/resources/tools/upscale.d.ts +23 -3
  94. package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
  95. package/dist/types/types.d.ts +249 -17
  96. package/package.json +2 -1
@@ -33,11 +33,76 @@
33
33
  * Multipart is not an optimisation. The object store sits behind Cloudflare on
34
34
  * a plan that caps a request body at roughly 100 MB, so anything larger has no
35
35
  * other way in.
36
+ *
37
+ * ## Progress, and why it ticks per transfer rather than per byte
38
+ *
39
+ * The bytes go to a presigned MinIO URL. Rails is never in the data path, and
40
+ * neither is rack-attack - so a progress bar here is a pure client-side
41
+ * problem, and the SDK solves it as far as `fetch` allows and no further.
42
+ *
43
+ * `fetch` has no upload-progress event. The two ways out were weighed and both
44
+ * were rejected for the core:
45
+ *
46
+ * - **XHR**, which is what the web frontend's axios uses today, is the only API
47
+ * that reports request bytes as they leave. It does not exist in a
48
+ * Cloudflare-Worker-class isolate, and this package must load there.
49
+ * - **A counting `ReadableStream` request body** would run in an isolate, and
50
+ * still does not work HERE. A stream body forces chunked transfer encoding,
51
+ * while a presigned PUT is signed over a fixed `Content-Length` (and, on the
52
+ * direct tier, over a `Content-MD5` covering the whole body), so the store
53
+ * answers a signature error. It also needs `duplex: "half"` and HTTP/2, which
54
+ * Safari and Firefox do not have. And it would count bytes handed to the
55
+ * runtime rather than bytes the store acknowledged - the lie that makes a bar
56
+ * sit at 100% for a minute.
57
+ *
58
+ * So: every `onProgress` in this module reports a COMPLETED transfer. On the
59
+ * multipart tier that is one tick per part, which the server sizes at 32 MiB,
60
+ * so a 1 GB file moves the bar 32 times. On the direct tier it is one tick for
61
+ * the whole file, which is at most 32 MiB by construction.
62
+ *
63
+ * A host that wants true byte-level granularity does not have to give up the
64
+ * driver: {@link UploadManagerOptions.fetch} takes the transport used for the
65
+ * presigned PUTs, so a browser can hand in an XHR-backed {@link FetchLike} and
66
+ * keep everything else. See the note there for the recipe. Building the whole
67
+ * flow by hand is also supported - {@link UploadManager.createBatch},
68
+ * {@link UploadManager.putDirect}, {@link UploadManager.attachBlobs} and the
69
+ * `multipart*` methods are public precisely so that a caller can drive the
70
+ * three phases itself and count bytes however it likes.
36
71
  */
37
72
  import { Resource, type ApiClient } from "../../http";
38
73
  import { type FetchLike, type FileInput, type Id, type OperationOptions, type ProgressCallback, type RequestOptions } from "../../types";
39
74
  import type { FsNode } from "../storage";
40
- /** Files at or above this size take the multipart path. Mirrors the backend. */
75
+ /**
76
+ * Files at or above this size take the multipart path: 32 MiB, exactly.
77
+ *
78
+ * This is a PROTOCOL constant, not a tuning knob, and it is the SDK's public
79
+ * copy of it. Import this rather than writing `32 * 1024 * 1024` again:
80
+ *
81
+ * ```ts
82
+ * import { MULTIPART_THRESHOLD } from "@omelhorsite/sdk";
83
+ * ```
84
+ *
85
+ * The number was verified in all three places it is currently written down, and
86
+ * as of this writing they agree:
87
+ *
88
+ * - `FsServices::NodeBatchCreator::MULTIPART_THRESHOLD = 32.megabytes`, which
89
+ * is the only one that decides anything. The server compares
90
+ * `entry[:size] >= MULTIPART_THRESHOLD` and puts `strategy` in the plan.
91
+ * - the web frontend's `lib/upload_core.ts`, which keeps its own literal.
92
+ * - here.
93
+ *
94
+ * The client-side copies exist because the CHECKSUM has to be in the manifest
95
+ * before the server has decided anything: below the threshold the digest is
96
+ * mandatory (it is Content-MD5-bound into the presigned signature), at or above
97
+ * it is pointless (multipart verifies per-part ETags). So the comparison is
98
+ * made twice, and {@link manifestEntryFor} uses `<` against exactly the
99
+ * server's `>=`.
100
+ *
101
+ * Drift is expensive and silent in one direction: a client that thinks the
102
+ * threshold is HIGHER than the server's omits the checksum on a file the server
103
+ * still plans as direct, and the upload dies at MinIO with a signature error
104
+ * that never mentions checksums. That is the whole reason this is exported.
105
+ */
41
106
  export declare const MULTIPART_THRESHOLD: number;
42
107
  /**
43
108
  * Part size for the multipart path. Mirrors the backend, but it is only a
@@ -190,6 +255,12 @@ export interface UploadInput {
190
255
  * Read the finished nodes back with one extra listing per batch. On by
191
256
  * default, and worth it: a normal listing hides nodes whose bytes never
192
257
  * landed, so a node coming back at all is proof the blob is bound.
258
+ *
259
+ * Turning it off costs more than the listing. `UploadResult.node` is then
260
+ * `null` for every file, and `oms.storage.upload()` - which returns exactly
261
+ * the nodes it read back - answers with an EMPTY array on a run where every
262
+ * byte landed. Use {@link UploadManager.upload} directly if you switch this
263
+ * off, and read `fs_node_id` instead.
193
264
  */
194
265
  readonly verify?: boolean;
195
266
  /** Replacement for the built-in MD5. See {@link Md5Base64Fn}. */
@@ -238,11 +309,40 @@ export declare class StorageRateGate {
238
309
  /** Constructor options for {@link UploadManager}. */
239
310
  export interface UploadManagerOptions {
240
311
  /**
241
- * The fetch used for the presigned PUTs.
312
+ * The fetch used for the presigned PUTs at the object store.
242
313
  *
243
314
  * Defaults to the one the {@link ApiClient} was built with, so bytes travel
244
- * on the same transport as everything else. Pass one only to send the object
245
- * store's traffic somewhere different from the API's.
315
+ * on the same transport as everything else. Only the PUTs go through it; the
316
+ * control plane always uses the client's own transport.
317
+ *
318
+ * This is also the supported way to get BYTE-LEVEL upload progress in a
319
+ * browser, which `fetch` cannot give (see the module note). Wrap XHR in a
320
+ * {@link FetchLike}, hand it in here, and keep the rest of the driver:
321
+ *
322
+ * ```ts
323
+ * const uploads = new UploadManager(oms.http, {
324
+ * fetch: (url, init) =>
325
+ * new Promise((resolve, reject) => {
326
+ * const xhr = new XMLHttpRequest();
327
+ * xhr.open(init?.method ?? "PUT", url);
328
+ * for (const [k, v] of Object.entries(init?.headers ?? {})) xhr.setRequestHeader(k, v as string);
329
+ * xhr.upload.onprogress = (event) => onBytes(url, event.loaded, event.total);
330
+ * xhr.onload = () =>
331
+ * resolve(new Response(xhr.response, { status: xhr.status, headers: parseXhrHeaders(xhr) }));
332
+ * xhr.onerror = () => reject(new Error("network"));
333
+ * xhr.send(init?.body as XMLHttpRequestBodyInit);
334
+ * }),
335
+ * });
336
+ * await uploads.upload({ parentId, files });
337
+ * ```
338
+ *
339
+ * Two things the wrapper MUST get right, both of which the SDK's own
340
+ * transport already does. It must not add an `Authorization` header or a
341
+ * cookie: the presigned signature is the credential and MinIO rejects a
342
+ * request carrying two authentication schemes. And it must expose `ETag` on
343
+ * the `Response` it builds, or the multipart tier has nothing to complete
344
+ * with - in a browser that additionally needs `ETag` in the bucket's
345
+ * `Access-Control-Expose-Headers`.
246
346
  */
247
347
  readonly fetch?: FetchLike;
248
348
  /** Replacement for the built-in MD5. See {@link Md5Base64Fn}. */
@@ -273,10 +373,16 @@ export declare class UploadManager extends Resource {
273
373
  * the server reports them. Only a structural failure (bad parent, empty or
274
374
  * oversized batch) throws.
275
375
  *
276
- * Progress granularity is a whole direct PUT or a whole multipart part, not
277
- * a byte counter. `fetch` has no upload-progress event and a streamed request
278
- * body is neither universally supported nor usable against a presigned PUT,
279
- * which needs a `Content-Length`.
376
+ * `options.onProgress` reports the WHOLE RUN: `loaded` counts bytes across
377
+ * every file and `total` is their sum, so the number only ever climbs. It
378
+ * ticks once per finished direct PUT and once per finished multipart part -
379
+ * never per byte, for the reasons in the module note - and it fires once with
380
+ * `loaded: 0` before anything is sent, so a bar can render immediately.
381
+ *
382
+ * That callback is deliberately NOT forwarded to the per-file drivers. They
383
+ * take an `onProgress` of their own that talks about ONE file, and letting
384
+ * the run-level callback reach them would interleave "3 MB of 3 MB" with
385
+ * "12 MB of 40 MB" on the same bar.
280
386
  *
281
387
  * Every multipart session this call opened is aborted before the error
282
388
  * leaves, so a torn run does not strand parts (and reserved quota) on the
@@ -292,6 +398,14 @@ export declare class UploadManager extends Resource {
292
398
  * (invalid parent, empty batch, over {@link MAX_BATCH}, a `..` segment)
293
399
  * is a 400.
294
400
  *
401
+ * Public because it is phase 1 of the protocol: a caller that wants its own
402
+ * progress accounting drives `createBatch` -> {@link putDirect} /
403
+ * {@link uploadMultipart} -> {@link attachBlobs} itself, and this is where it
404
+ * learns the per-file strategy and the byte counts it will be reporting
405
+ * against. Whatever it does, it must pace itself against `fs_upload` (300
406
+ * requests a minute, shared by every call in this class) - {@link gate} is
407
+ * exposed for exactly that.
408
+ *
295
409
  * @throws {OmsApiError} 400 on a structural problem, 404 when the parent is
296
410
  * not a directory the caller may write to.
297
411
  */
@@ -310,6 +424,16 @@ export declare class UploadManager extends Resource {
310
424
  * expired signature or a checksum mismatch is deterministic and a retry only
311
425
  * costs the bytes again.
312
426
  *
427
+ * Phase 2 of the protocol for a file below {@link MULTIPART_THRESHOLD}, and
428
+ * public so a caller can drive its own flow. `onProgress` fires EXACTLY ONCE
429
+ * here, after the store has accepted the body, with `loaded === total`. It is
430
+ * not a byte counter and cannot be one: see the module note, and
431
+ * {@link UploadManagerOptions.fetch} for the XHR escape hatch that can.
432
+ *
433
+ * Between this and {@link attachBlobs} the node exists with no bytes bound and
434
+ * is hidden from every listing, so a caller that stops here leaves a pending
435
+ * node behind.
436
+ *
313
437
  * @returns The ETag the store reported, or `null` when it was not readable -
314
438
  * which happens in a browser whose bucket CORS policy does not expose
315
439
  * `ETag`. The direct tier does not need it; multipart does.
@@ -322,8 +446,11 @@ export declare class UploadManager extends Resource {
322
446
  * their nodes. Until this lands the node exists with no bytes and is hidden
323
447
  * from every listing.
324
448
  *
325
- * Answers 200 with per-item results even when every item failed. Read
326
- * `attached` on each one; do not infer success from the status.
449
+ * Phase 3 of the protocol, and the last one for the direct tier. Answers 200
450
+ * with per-item results even when every item failed: read `attached` on each
451
+ * one, and never infer success from the status. A caller driving the flow by
452
+ * hand must not skip this - a node with no blob is invisible in listings and
453
+ * is eventually swept by `FsNodeUploadReaperJob`.
327
454
  */
328
455
  attachBlobs(attachments: Array<{
329
456
  fs_node_id: Id;
@@ -380,11 +507,30 @@ export declare class UploadManager extends Resource {
380
507
  * {@link MULTIPART_PART_SIZE}, so a change on the backend does not silently
381
508
  * corrupt an upload here.
382
509
  *
510
+ * Phases 2 and 3 at once for a file at or above {@link MULTIPART_THRESHOLD}:
511
+ * `multipart/complete` creates the blob row itself, so there is no
512
+ * {@link attachBlobs} call on this tier.
513
+ *
514
+ * This is the tier where a progress bar is actually useful, because the parts
515
+ * are 32 MiB and each one that lands is a real tick. `onProgress` reports
516
+ * THIS file cumulatively - `loaded` climbing towards `total === body.size` -
517
+ * and fires once up front so a bar can render before the first part is sent.
518
+ * On a resumed session it starts at the bytes already stored rather than at
519
+ * zero, which is the difference between "resuming at 60%" and a bar that
520
+ * appears to lose an hour of work. `onPart` is the lower-level twin: one call
521
+ * per part, with that part's byte count and its ETag.
522
+ *
523
+ * NOTHING here is per-byte; see the module note for why, and
524
+ * {@link UploadManagerOptions.fetch} for the way around it.
525
+ *
383
526
  * @param options.resume A session from a previous attempt plus the parts that
384
527
  * already landed. The matching parts are skipped.
385
528
  * @param options.onSession Called with the upload token as soon as there is
386
- * one, so a caller can persist it and abort later.
529
+ * one, so a caller can persist it and abort later. A failed run MUST reach
530
+ * {@link multipartAbort} with that token or the parts stay charged against
531
+ * the quota until a reaper notices.
387
532
  * @param options.onPart Called with the byte count of each finished part.
533
+ * @param options.onProgress Called with this file's cumulative byte count.
388
534
  */
389
535
  uploadMultipart(fsNodeId: Id, body: Blob, options?: RequestOptions & {
390
536
  concurrency?: number;
@@ -395,6 +541,7 @@ export declare class UploadManager extends Resource {
395
541
  };
396
542
  onSession?: (uploadToken: string, partSize: number) => void;
397
543
  onPart?: (bytes: number, part: MultipartPart) => void;
544
+ onProgress?: ProgressCallback;
398
545
  }): Promise<MultipartCompletion>;
399
546
  /**
400
547
  * PUTs bytes at the object store on the injected transport, with no
@@ -27,20 +27,52 @@
27
27
  * - the general 600/min for everything else.
28
28
  * - the direct PUTs at the object store, which rack-attack never sees at all.
29
29
  */
30
- import { Resource, type ApiClient } from "../http";
31
- import type { BaseRecord, FileInput, FileOutput, Id, OperationOptions, Paginated, PageParams, RequestOptions } from "../types";
30
+ /**
31
+ * The server's null sentinel: U+0008, a literal backspace.
32
+ *
33
+ * `CrudActions` rewrites any filter value equal to it before the query layer
34
+ * ever sees it - `transform_values! { |v| v == "\b" ? nil : v }`, applied to
35
+ * every option bag, not just to `exact_search`. Through `exact_search` that is
36
+ * exactly what is wanted: `where(parent_id: nil)`, the root nodes.
37
+ *
38
+ * Through `extra_options` the same rewrite is a trap, because
39
+ * `QueryExtraOptions::FsNodes` opens with
40
+ * `return unless params[:parent_id].present?` and `nil` is not present, so the
41
+ * filter is dropped without a word. {@link StorageNamespace.list} is built
42
+ * around that difference; do not collapse the two filters back together.
43
+ */
44
+ import { type ApiClient, Resource } from "../http";
45
+ import type { ListParams } from "../listing";
46
+ import type { BaseRecord, FileOutput, Id, OperationOptions, Paginated, RequestOptions } from "../types";
32
47
  import type { User } from "./account";
33
48
  import { StorageRateGate, UploadManager } from "./storage/upload";
49
+ import type { UploadInput } from "./storage/upload";
34
50
  /** What a node is. */
35
51
  export type FsNodeKind = "file" | "directory";
36
52
  /**
37
53
  * A node in the virtual filesystem.
38
54
  *
39
- * These are exactly the fields the server's blueprint emits - `id`,
40
- * `created_at`, `updated_at`, `name`, `parent_id`, `kind`, `size`, `max_size` -
41
- * and the `:extended` view adds nothing. In particular there is no
42
- * `content_type` and no `path`: the type of a file is decided from its name at
43
- * download time, and the path is gone.
55
+ * This is the WHOLE record, and it was checked against the blueprint CHAIN
56
+ * rather than against the table, because those two disagree.
57
+ * `FsNodeBlueprint` declares `name`, `parent_id`, `kind`, `size`,
58
+ * `max_size`; it extends `ApplicationBlueprint`, which contributes `id`,
59
+ * `created_at` and `updated_at`; and its `:extended` view has an EMPTY body,
60
+ * which in Blueprinter inherits the base fields rather than emitting nothing.
61
+ * So `list`, `get`, `create` and `update` all answer the same eight fields.
62
+ * Reading a view name and assuming it adds something has already produced bugs
63
+ * in this repo - follow the `<` before deciding a field does or does not exist.
64
+ *
65
+ * The `fs_nodes` TABLE is wider than that, and the difference is never sent:
66
+ * `creator_id`, `updater_id`, `destroyer_id`, `signed_url_generated`,
67
+ * `is_vault_root` and the `id_path` ltree are all real columns that appear in
68
+ * no view. Declaring them client-side is worse than leaving them out, because
69
+ * every later reader then believes they arrive.
70
+ *
71
+ * There is no `data` and no `url` field either: bytes are reached through
72
+ * {@link StorageNamespace.download}, {@link StorageNamespace.downloadStream} or
73
+ * {@link StorageNamespace.downloadUrl}, never off the record. And no
74
+ * `content_type` and no `path` - the type is decided from the name at download
75
+ * time, and the `path` column was dropped in the ltree migration.
44
76
  */
45
77
  export interface FsNode extends BaseRecord {
46
78
  readonly name: string;
@@ -94,12 +126,27 @@ export interface FsStream {
94
126
  readonly size: number | undefined;
95
127
  }
96
128
  /** Filters for {@link StorageNamespace.list}. */
97
- export interface ListFsNodesParams extends PageParams {
129
+ /** Filter columns of `GET /fs_nodes`. */
130
+ export declare const FS_NODE_FILTER_COLUMNS: readonly ["id", "created_at", "updated_at", "name", "parent_id"];
131
+ /** `extra_options` keys of `GET /fs_nodes`. */
132
+ export declare const FS_NODE_EXTRA_OPTION_KEYS: readonly ["parent_id", "include_pending"];
133
+ /**
134
+ * `extra_options` of `GET /fs_nodes`. `parent_id` here returns the folder's
135
+ * children AND the folder itself; the `parent_id` column in `exactSearch`
136
+ * returns the children only, and is the only one that can say `null`.
137
+ */
138
+ export interface FsNodeExtraOptions {
139
+ readonly parent_id?: Id;
140
+ readonly include_pending?: boolean;
141
+ }
142
+ /** Filters for {@link StorageNamespace.list}. */
143
+ export interface ListFsNodesParams extends ListParams<(typeof FS_NODE_FILTER_COLUMNS)[number], FsNodeExtraOptions> {
98
144
  /**
99
145
  * Directory to list. `null` lists the caller's ROOT nodes (home, trash,
100
- * vault), which the server selects through its `\b` null sentinel.
146
+ * vault). Omitted, no parent filter is applied and the listing spans the
147
+ * caller's whole tree, which is what a name search across folders wants.
101
148
  */
102
- readonly parentId: Id | null;
149
+ readonly parentId?: Id | null;
103
150
  /**
104
151
  * Include nodes whose bytes never landed. Off by default, because a
105
152
  * half-finished upload is not something a user wants to see - and because a
@@ -109,6 +156,12 @@ export interface ListFsNodesParams extends PageParams {
109
156
  /**
110
157
  * Also return the parent itself, so one call gets both the folder's metadata
111
158
  * and its children. It occupies a slot on the page like any other row.
159
+ *
160
+ * IGNORED when `parentId` is `null`, and that is a correction rather than a
161
+ * convenience: the server-side filter behind this flag cannot express "no
162
+ * parent" at all, and asking it to used to answer with the caller's whole
163
+ * tree. The roots have no folder to fold in anyway. The detail is on
164
+ * {@link StorageNamespace.list}.
112
165
  */
113
166
  readonly includeSelf?: boolean;
114
167
  }
@@ -128,6 +181,8 @@ export interface CopyFsNodesInput {
128
181
  readonly ids: Id[];
129
182
  readonly newParentId: Id;
130
183
  }
184
+ /** `GET /fs_grants` filters on {@link BASE_FILTER_COLUMNS} only. */
185
+ export type ListFsGrantsParams = ListParams<never>;
131
186
  /**
132
187
  * A sharing grant on a node.
133
188
  *
@@ -192,7 +247,7 @@ export declare class FsGrantsNamespace extends Resource {
192
247
  *
193
248
  * @throws {OmsAuthError} 401 when anonymous.
194
249
  */
195
- list(params?: PageParams, options?: RequestOptions): Promise<Paginated<FsGrant>>;
250
+ list(params?: ListFsGrantsParams, options?: RequestOptions): Promise<Paginated<FsGrant>>;
196
251
  /** `GET /fs_grants/:id` - one grant, with its node, grantor and grantee expanded. */
197
252
  get(id: Id, options?: RequestOptions): Promise<FsGrant>;
198
253
  /**
@@ -252,19 +307,45 @@ export declare class StorageNamespace extends Resource {
252
307
  */
253
308
  roots(options?: RequestOptions): Promise<FsRoots>;
254
309
  /**
255
- * `GET /fs_nodes` - the children of a directory.
310
+ * `GET /fs_nodes` - the children of a directory, or the roots when
311
+ * `parentId` is `null`.
256
312
  *
257
313
  * Anonymous callers get an empty listing, always: the listing scope is the
258
314
  * caller's own tree plus what was explicitly shared with them, and a public
259
315
  * grant is deliberately NOT enumerable. Reach a publicly-shared node by id
260
316
  * with {@link get} or {@link shared} instead.
261
317
  *
318
+ * TWO server-side filters address a directory and they are NOT
319
+ * interchangeable. That asymmetry is the whole subtlety of this method:
320
+ *
321
+ * - `exact_search[parent_id]` reaches `Searchable.exact_search`, which is a
322
+ * bare `where(params)`. The controller has already rewritten a `\b` value
323
+ * to `nil` by then, so the sentinel lands as `WHERE parent_id IS NULL` -
324
+ * and since `FsNode.root_nodes` is exactly `where(parent_id: nil)`, this is
325
+ * the only filter in the API that can say "the roots". The ltree
326
+ * `id_path` is the source of truth for ANCESTRY, but rootness is still a
327
+ * null `parent_id`.
328
+ * - `extra_options[parent_id]` reaches `QueryExtraOptions::FsNodes`, which
329
+ * runs `where(parent_id: x).or(where(id: x))` and so folds the folder
330
+ * itself back into its own listing. Convenient - and guarded by
331
+ * `return unless params[:parent_id].present?`, with that same `\b` -> `nil`
332
+ * rewrite happening first. Hand it the sentinel and the guard drops the
333
+ * filter IN SILENCE. The request still answers 200; it just answers with
334
+ * the caller's ENTIRE listable tree, page after page, instead of three
335
+ * rows. It is the same failure shape as the `inside_path` incident that
336
+ * `reject_unknown_filter_keys!` was written for, except this key IS known,
337
+ * so nothing rejects it.
338
+ *
339
+ * Hence `includeSelf` is honoured under a real directory and ignored at the
340
+ * top of the tree. Not client-side taste: it is the only combination the
341
+ * server can actually express.
342
+ *
262
343
  * The endpoint is conditional-GET aware and answers 304 to a matching
263
344
  * `If-None-Match`. The SDK never sends one, and asks the runtime not to
264
345
  * revalidate on its own, because a 304 has no body and would surface here as
265
346
  * an error rather than as an empty page.
266
347
  */
267
- list(params: ListFsNodesParams, options?: RequestOptions): Promise<Paginated<FsNode>>;
348
+ list(params?: ListFsNodesParams, options?: RequestOptions): Promise<Paginated<FsNode>>;
268
349
  /**
269
350
  * `GET /fs_nodes/:id` - one node.
270
351
  *
@@ -312,25 +393,28 @@ export declare class StorageNamespace extends Resource {
312
393
  * intake, presigns, sends the bytes straight to object storage, binds the
313
394
  * blobs and reads the finished nodes back.
314
395
  *
315
- * Files at or above 32 MiB take the multipart path automatically, and that is
316
- * not tuning: the object store sits behind Cloudflare with a request-body cap
317
- * around 100 MB, so it is the only way a large file gets in.
396
+ * Files at or above `MULTIPART_THRESHOLD` (32 MiB, exported from this
397
+ * package) take the multipart path automatically, and that is not tuning:
398
+ * the object store sits behind Cloudflare with a request-body cap around
399
+ * 100 MB, so it is the only way a large file gets in at all.
318
400
  *
319
401
  * A per-file rejection - a quota that ran out, a name that collides with a
320
402
  * directory - does not throw. It comes back in
321
403
  * {@link UploadManager.upload}'s results, which is why that method is the one
322
404
  * to call when partial success matters; this wrapper returns only the nodes
323
- * that landed.
324
- *
325
- * Progress arrives per finished part or file, not per byte: `fetch` exposes
326
- * no upload-progress event.
327
- */
328
- upload(input: {
329
- parentId: Id;
330
- files: FileInput[];
331
- relativePaths?: string[];
332
- concurrency?: number;
333
- }, options?: OperationOptions): Promise<FsNode[]>;
405
+ * that landed, and throws only when EVERY file was refused.
406
+ *
407
+ * `options.onProgress` counts bytes across the whole run and only ever
408
+ * climbs. It ticks once per finished direct PUT and once per finished
409
+ * multipart part, never per byte - `fetch` has no upload-progress event, and
410
+ * the alternatives are argued out in `storage/upload.ts`. A caller that needs
411
+ * true byte granularity in a browser builds an `UploadManager` with an
412
+ * XHR-backed transport; a caller that wants to drive the three phases itself
413
+ * has {@link UploadManager.createBatch}, {@link UploadManager.putDirect},
414
+ * {@link UploadManager.attachBlobs} and the `multipart*` methods, all public
415
+ * for that purpose.
416
+ */
417
+ upload(input: UploadInput, options?: OperationOptions): Promise<FsNode[]>;
334
418
  /**
335
419
  * `POST /fs_nodes/create_directories` - creates a subtree in one call.
336
420
  *
@@ -11,7 +11,8 @@
11
11
  * in the same request.
12
12
  */
13
13
  import { type ApiClient, Resource } from "../http";
14
- import type { FileInput, Id, Paginated, PageParams, RequestOptions, Timestamp } from "../types";
14
+ import type { ListParams } from "../listing";
15
+ import type { FileInput, Id, Paginated, RequestOptions, Timestamp } from "../types";
15
16
  /** Lifecycle of a ticket. */
16
17
  export type TicketStatus = "open" | "closed";
17
18
  /**
@@ -99,29 +100,37 @@ export interface CreateTicketInput {
99
100
  readonly initialMessage?: string;
100
101
  readonly context?: TicketContext;
101
102
  /**
102
- * Files to attach. The SDK turns each one into the data URI the endpoint
103
- * expects, so every attachment is buffered in memory.
103
+ * Files to attach, as bytes or as an already-encoded data URL. Bytes are
104
+ * base64-encoded here, so every attachment is buffered in memory.
104
105
  *
105
- * Backend caps: {@link TICKET_MAX_ATTACHMENTS} files,
106
+ * Server caps: {@link TICKET_MAX_ATTACHMENTS} files,
106
107
  * {@link TICKET_MAX_ATTACHMENT_BYTES} in total, `image/*` and `video/*`
107
- * only - and an attachment that breaks any of them is dropped in SILENCE,
108
- * with the ticket still answering 201. The SDK therefore validates before
109
- * sending and raises rather than let a screenshot disappear.
108
+ * only. An attachment over a cap is dropped server-side with the ticket
109
+ * still answering 201, so the SDK validates first and throws instead.
110
110
  */
111
- readonly attachments?: FileInput[];
111
+ readonly attachments?: ReadonlyArray<FileInput | TicketAttachmentDataUrl>;
112
+ }
113
+ /** An attachment that is already a `data:<mime>;base64,...` URL. */
114
+ export interface TicketAttachmentDataUrl {
115
+ readonly dataUrl: string;
116
+ readonly filename: string;
112
117
  }
113
118
  /** Fields that can change after a ticket exists. */
114
119
  export interface UpdateTicketInput {
115
120
  readonly status?: TicketStatus;
116
121
  }
122
+ /** Filter columns of `GET /tickets`, on top of {@link BASE_FILTER_COLUMNS}. */
123
+ export declare const TICKET_FILTER_COLUMNS: readonly ["status", "user_id"];
117
124
  /** Filters for {@link TicketsNamespace.list}. */
118
- export interface ListTicketsParams extends PageParams {
125
+ export interface ListTicketsParams extends ListParams<(typeof TICKET_FILTER_COLUMNS)[number]> {
119
126
  readonly status?: TicketStatus;
120
127
  /** Administrators only: someone else's tickets. */
121
128
  readonly userId?: Id;
122
129
  }
130
+ /** Filter columns of `GET /ticket_messages`, on top of {@link BASE_FILTER_COLUMNS}. */
131
+ export declare const TICKET_MESSAGE_FILTER_COLUMNS: readonly ["ticket_id", "sender_id", "created_at"];
123
132
  /** Filters for {@link TicketMessagesNamespace.list}. */
124
- export interface ListTicketMessagesParams extends PageParams {
133
+ export interface ListTicketMessagesParams extends ListParams<(typeof TICKET_MESSAGE_FILTER_COLUMNS)[number]> {
125
134
  readonly ticketId: TicketId;
126
135
  }
127
136
  /** Arguments for replying to a ticket. */
@@ -213,15 +222,94 @@ export declare class TicketsNamespace extends Resource {
213
222
  /**
214
223
  * `GET /tickets/:id/attachment/:blob_id` - one attachment's bytes.
215
224
  *
216
- * The endpoint answers 302 towards object storage and `fetch` follows it.
217
- * The redirect is cross-origin, so the platform drops the `Authorization`
218
- * header on the way: the credential never reaches the storage host.
225
+ * Unlike `account.picture` and `chests.entries.download`, this one CANNOT be
226
+ * asked for anonymously. The action resolves the ticket through
227
+ * `Ticket.viewable_by(Current.user)` and declares `oauth_scope
228
+ * "tickets:write"`, so a request with no credential is a 404 on someone
229
+ * else's ticket and a 401 on your own. The credential has to ride the first
230
+ * hop, which is exactly what makes this awkward.
231
+ *
232
+ * The awkward part. Rails answers `302` to `minio.omelhorsite.pt` and `fetch`
233
+ * follows it. Two different things then happen depending on how the client
234
+ * was built:
235
+ *
236
+ * - **token mode** (`credentials: "omit"`). The Fetch standard strips
237
+ * `Authorization` on a cross-origin redirect, so the store gets a clean
238
+ * presigned request. The hop also swaps the origin for an opaque one, so
239
+ * MinIO sees `Origin: null` and answers `Access-Control-Allow-Origin: *` -
240
+ * which an uncredentialed request accepts. This works, in a browser and
241
+ * out of one.
242
+ * - **cookie mode** (`sessionCookie: true`, the production web app). Same
243
+ * `Origin: null`, same `*` back, but now the request carries credentials,
244
+ * and wildcard plus credentials is illegal in CORS regardless of
245
+ * `Access-Control-Allow-Credentials`. The browser rejects the response and
246
+ * `fetch` rejects with an opaque "Failed to fetch".
247
+ *
248
+ * And it cannot be repaired the way the other two were, because the obvious
249
+ * repair does not exist in a browser: `redirect: "manual"` there yields an
250
+ * OPAQUE-REDIRECT response - status 0, empty header list - so `Location` is
251
+ * unreadable and the second hop cannot be re-issued without credentials.
252
+ * `redirect: "error"` and `XMLHttpRequest` are no better. Reading `Location`
253
+ * works only off-browser, where nothing was broken to begin with. The other
254
+ * two endpoints escape by needing no credential at all; this one has no such
255
+ * escape, and the backend exposes no `..._url` companion route the way
256
+ * `fs_nodes` does with `data_url`.
257
+ *
258
+ * So in a browser in cookie mode, use {@link attachmentUrl} and let the
259
+ * platform fetch the bytes - an `<img>`, a `<video>`, an `<a download>` make
260
+ * no-cors requests and follow the redirect with no CORS check at all. This
261
+ * method stays for every other context, and turns the browser failure into an
262
+ * error that says so instead of an unexplained network fault.
219
263
  *
220
264
  * Take `blobId` from {@link TicketAttachment.blob_id}; the filename and
221
265
  * content type are on the same record, which is why this hands back bare
222
266
  * bytes.
267
+ *
268
+ * @throws {OmsError} `unsupported` when the redirect was blocked by CORS,
269
+ * naming {@link attachmentUrl} as the way through.
270
+ * @throws {OmsApiError} 404 when the blob is not on that ticket, or the
271
+ * ticket is not yours.
223
272
  */
224
273
  attachment(id: TicketId, blobId: number, options?: RequestOptions): Promise<Blob>;
274
+ /**
275
+ * Absolute URL for one attachment, for an `<img>`, an `<a download>` or a new
276
+ * tab. In a browser in cookie mode this is the ONLY way to get at the bytes;
277
+ * see {@link attachment} for why.
278
+ *
279
+ * Asynchronous, unlike `account.pictureUrl` and
280
+ * `chests.entries.downloadUrl`, and the difference is not an accident: those
281
+ * two routes are anonymous, this one is not, so a credential has to be
282
+ * resolved before the URL means anything. Resolving it may involve a token
283
+ * refresh, which is why it cannot be a getter.
284
+ *
285
+ * How the URL authenticates depends on the client, matching what the backend
286
+ * accepts (`Session.candidate_tokens` reads the `Authorization` header, then
287
+ * `?token=`, then the `oms_session` cookie):
288
+ *
289
+ * - **cookie mode**: no credential in the URL. The browser attaches the
290
+ * `oms_session` cookie itself. It is host-only on the API host and
291
+ * `SameSite=Lax`, so this works from a page on `omelhorsite.pt` or another
292
+ * host under it, and from nowhere else. Do not add `crossorigin` to the
293
+ * element: it turns a no-cors load into a CORS one and re-creates the exact
294
+ * failure this method exists to avoid.
295
+ * - **token mode**: the token is appended as `?token=`. Rails accepts a query
296
+ * credential on API routes precisely because a native client has no cookie
297
+ * jar and an `<img>` cannot carry a header.
298
+ *
299
+ * THE TOKEN-MODE URL CONTAINS A LIVE CREDENTIAL. It goes into the DOM, into
300
+ * the server access log, and into anything that records URLs. Build it at the
301
+ * moment of use, never store it, never log it, and never put it somewhere
302
+ * another person can read - anyone holding it holds the session until it is
303
+ * revoked. When the URL is destined for something outside your own page,
304
+ * fetch the bytes with {@link attachment} and hand over a `blob:` URL
305
+ * instead.
306
+ *
307
+ * ```tsx
308
+ * const src = await oms.tickets.attachmentUrl(ticket.id, file.blob_id);
309
+ * <img src={src} alt={file.filename} />
310
+ * ```
311
+ */
312
+ attachmentUrl(id: TicketId, blobId: number): Promise<string>;
225
313
  }
226
314
  /**
227
315
  * Encodes a file as a `data:<mime>;base64,<...>` URI.
@@ -14,10 +14,25 @@
14
14
  import { Resource } from "../../http";
15
15
  import type { FileInput, Id, RequestOptions } from "../../types";
16
16
  import { type ToolCaptcha, type ToolJobHandle, type ToolRecord, type ToolRunOptions } from "./index";
17
- /** A background removal run. */
17
+ /**
18
+ * A background removal run.
19
+ *
20
+ * Both routes that answer with one - `POST /background_removals` and
21
+ * `GET /background_removals/:id` - render the `:extended` view, so `result_url`
22
+ * is always PRESENT and simply `null` until the run completes.
23
+ *
24
+ * `progress_percent`, inherited from {@link ToolRecord}, is never sent for this
25
+ * tool: `BackgroundRemovalBlueprint` has no such field. Progress lives on the
26
+ * {@link Job} row that {@link BackgroundRemovalCreated.job_id} names.
27
+ */
18
28
  export interface BackgroundRemoval extends ToolRecord {
19
- /** URL of the cut-out PNG, once the status is `"complete"`. */
20
- readonly result_url?: string | null;
29
+ /**
30
+ * Signed URL of the cut-out PNG, or `null` - for the run not having
31
+ * finished, for it having failed, or for the 24-hour sweep having taken the
32
+ * attachment. {@link BackgroundRemovalNamespace.resultUrl} tells the three
33
+ * apart.
34
+ */
35
+ readonly result_url: string | null;
21
36
  }
22
37
  /** What `POST /background_removals` answers with. */
23
38
  export type BackgroundRemovalCreated = BackgroundRemoval & ToolJobHandle;