@omelhorsite/sdk 0.2.0 → 0.3.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 (44) hide show
  1. package/dist/index.js +4939 -552
  2. package/dist/types/client.d.ts +60 -3
  3. package/dist/types/http.d.ts +444 -19
  4. package/dist/types/index.d.ts +4 -1
  5. package/dist/types/resources/account.d.ts +66 -3
  6. package/dist/types/resources/admin.d.ts +1837 -0
  7. package/dist/types/resources/auth/index.d.ts +39 -0
  8. package/dist/types/resources/auth/passkeys.d.ts +652 -0
  9. package/dist/types/resources/auth/sessions.d.ts +847 -0
  10. package/dist/types/resources/chests.d.ts +54 -3
  11. package/dist/types/resources/content.d.ts +2970 -0
  12. package/dist/types/resources/dynamicQrs.d.ts +39 -3
  13. package/dist/types/resources/forms.d.ts +176 -35
  14. package/dist/types/resources/index.d.ts +19 -8
  15. package/dist/types/resources/ipLookup.d.ts +20 -4
  16. package/dist/types/resources/jobs.d.ts +62 -21
  17. package/dist/types/resources/library.d.ts +1435 -0
  18. package/dist/types/resources/linkTrees.d.ts +142 -30
  19. package/dist/types/resources/media.d.ts +351 -0
  20. package/dist/types/resources/movies.d.ts +1186 -0
  21. package/dist/types/resources/music/artists.d.ts +1066 -0
  22. package/dist/types/resources/music/imports.d.ts +940 -0
  23. package/dist/types/resources/music/index.d.ts +61 -0
  24. package/dist/types/resources/music/playlists.d.ts +1026 -0
  25. package/dist/types/resources/music/social.d.ts +1132 -0
  26. package/dist/types/resources/music/songs.d.ts +1183 -0
  27. package/dist/types/resources/notepads.d.ts +4 -1
  28. package/dist/types/resources/quotas.d.ts +7 -1
  29. package/dist/types/resources/realtime.d.ts +855 -0
  30. package/dist/types/resources/shortLinks.d.ts +45 -4
  31. package/dist/types/resources/social.d.ts +1330 -0
  32. package/dist/types/resources/storage/upload.d.ts +158 -11
  33. package/dist/types/resources/storage.d.ts +88 -22
  34. package/dist/types/resources/tickets.d.ts +82 -3
  35. package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
  36. package/dist/types/resources/tools/captions.d.ts +448 -21
  37. package/dist/types/resources/tools/downloader.d.ts +21 -0
  38. package/dist/types/resources/tools/index.d.ts +57 -15
  39. package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
  40. package/dist/types/resources/tools/transcription.d.ts +35 -13
  41. package/dist/types/resources/tools/upscale.d.ts +23 -3
  42. package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
  43. package/dist/types/types.d.ts +249 -17
  44. 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,51 @@
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 { BaseRecord, FileOutput, Id, OperationOptions, Paginated, PageParams, RequestOptions } from "../types";
32
46
  import type { User } from "./account";
33
47
  import { StorageRateGate, UploadManager } from "./storage/upload";
48
+ import type { UploadInput } from "./storage/upload";
34
49
  /** What a node is. */
35
50
  export type FsNodeKind = "file" | "directory";
36
51
  /**
37
52
  * A node in the virtual filesystem.
38
53
  *
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.
54
+ * This is the WHOLE record, and it was checked against the blueprint CHAIN
55
+ * rather than against the table, because those two disagree.
56
+ * `FsNodeBlueprint` declares `name`, `parent_id`, `kind`, `size`,
57
+ * `max_size`; it extends `ApplicationBlueprint`, which contributes `id`,
58
+ * `created_at` and `updated_at`; and its `:extended` view has an EMPTY body,
59
+ * which in Blueprinter inherits the base fields rather than emitting nothing.
60
+ * So `list`, `get`, `create` and `update` all answer the same eight fields.
61
+ * Reading a view name and assuming it adds something has already produced bugs
62
+ * in this repo - follow the `<` before deciding a field does or does not exist.
63
+ *
64
+ * The `fs_nodes` TABLE is wider than that, and the difference is never sent:
65
+ * `creator_id`, `updater_id`, `destroyer_id`, `signed_url_generated`,
66
+ * `is_vault_root` and the `id_path` ltree are all real columns that appear in
67
+ * no view. Declaring them client-side is worse than leaving them out, because
68
+ * every later reader then believes they arrive.
69
+ *
70
+ * There is no `data` and no `url` field either: bytes are reached through
71
+ * {@link StorageNamespace.download}, {@link StorageNamespace.downloadStream} or
72
+ * {@link StorageNamespace.downloadUrl}, never off the record. And no
73
+ * `content_type` and no `path` - the type is decided from the name at download
74
+ * time, and the `path` column was dropped in the ltree migration.
44
75
  */
45
76
  export interface FsNode extends BaseRecord {
46
77
  readonly name: string;
@@ -109,6 +140,12 @@ export interface ListFsNodesParams extends PageParams {
109
140
  /**
110
141
  * Also return the parent itself, so one call gets both the folder's metadata
111
142
  * and its children. It occupies a slot on the page like any other row.
143
+ *
144
+ * IGNORED when `parentId` is `null`, and that is a correction rather than a
145
+ * convenience: the server-side filter behind this flag cannot express "no
146
+ * parent" at all, and asking it to used to answer with the caller's whole
147
+ * tree. The roots have no folder to fold in anyway. The detail is on
148
+ * {@link StorageNamespace.list}.
112
149
  */
113
150
  readonly includeSelf?: boolean;
114
151
  }
@@ -252,13 +289,39 @@ export declare class StorageNamespace extends Resource {
252
289
  */
253
290
  roots(options?: RequestOptions): Promise<FsRoots>;
254
291
  /**
255
- * `GET /fs_nodes` - the children of a directory.
292
+ * `GET /fs_nodes` - the children of a directory, or the roots when
293
+ * `parentId` is `null`.
256
294
  *
257
295
  * Anonymous callers get an empty listing, always: the listing scope is the
258
296
  * caller's own tree plus what was explicitly shared with them, and a public
259
297
  * grant is deliberately NOT enumerable. Reach a publicly-shared node by id
260
298
  * with {@link get} or {@link shared} instead.
261
299
  *
300
+ * TWO server-side filters address a directory and they are NOT
301
+ * interchangeable. That asymmetry is the whole subtlety of this method:
302
+ *
303
+ * - `exact_search[parent_id]` reaches `Searchable.exact_search`, which is a
304
+ * bare `where(params)`. The controller has already rewritten a `\b` value
305
+ * to `nil` by then, so the sentinel lands as `WHERE parent_id IS NULL` -
306
+ * and since `FsNode.root_nodes` is exactly `where(parent_id: nil)`, this is
307
+ * the only filter in the API that can say "the roots". The ltree
308
+ * `id_path` is the source of truth for ANCESTRY, but rootness is still a
309
+ * null `parent_id`.
310
+ * - `extra_options[parent_id]` reaches `QueryExtraOptions::FsNodes`, which
311
+ * runs `where(parent_id: x).or(where(id: x))` and so folds the folder
312
+ * itself back into its own listing. Convenient - and guarded by
313
+ * `return unless params[:parent_id].present?`, with that same `\b` -> `nil`
314
+ * rewrite happening first. Hand it the sentinel and the guard drops the
315
+ * filter IN SILENCE. The request still answers 200; it just answers with
316
+ * the caller's ENTIRE listable tree, page after page, instead of three
317
+ * rows. It is the same failure shape as the `inside_path` incident that
318
+ * `reject_unknown_filter_keys!` was written for, except this key IS known,
319
+ * so nothing rejects it.
320
+ *
321
+ * Hence `includeSelf` is honoured under a real directory and ignored at the
322
+ * top of the tree. Not client-side taste: it is the only combination the
323
+ * server can actually express.
324
+ *
262
325
  * The endpoint is conditional-GET aware and answers 304 to a matching
263
326
  * `If-None-Match`. The SDK never sends one, and asks the runtime not to
264
327
  * revalidate on its own, because a 304 has no body and would surface here as
@@ -312,25 +375,28 @@ export declare class StorageNamespace extends Resource {
312
375
  * intake, presigns, sends the bytes straight to object storage, binds the
313
376
  * blobs and reads the finished nodes back.
314
377
  *
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.
378
+ * Files at or above `MULTIPART_THRESHOLD` (32 MiB, exported from this
379
+ * package) take the multipart path automatically, and that is not tuning:
380
+ * the object store sits behind Cloudflare with a request-body cap around
381
+ * 100 MB, so it is the only way a large file gets in at all.
318
382
  *
319
383
  * A per-file rejection - a quota that ran out, a name that collides with a
320
384
  * directory - does not throw. It comes back in
321
385
  * {@link UploadManager.upload}'s results, which is why that method is the one
322
386
  * 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[]>;
387
+ * that landed, and throws only when EVERY file was refused.
388
+ *
389
+ * `options.onProgress` counts bytes across the whole run and only ever
390
+ * climbs. It ticks once per finished direct PUT and once per finished
391
+ * multipart part, never per byte - `fetch` has no upload-progress event, and
392
+ * the alternatives are argued out in `storage/upload.ts`. A caller that needs
393
+ * true byte granularity in a browser builds an `UploadManager` with an
394
+ * XHR-backed transport; a caller that wants to drive the three phases itself
395
+ * has {@link UploadManager.createBatch}, {@link UploadManager.putDirect},
396
+ * {@link UploadManager.attachBlobs} and the `multipart*` methods, all public
397
+ * for that purpose.
398
+ */
399
+ upload(input: UploadInput, options?: OperationOptions): Promise<FsNode[]>;
334
400
  /**
335
401
  * `POST /fs_nodes/create_directories` - creates a subtree in one call.
336
402
  *
@@ -213,15 +213,94 @@ export declare class TicketsNamespace extends Resource {
213
213
  /**
214
214
  * `GET /tickets/:id/attachment/:blob_id` - one attachment's bytes.
215
215
  *
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.
216
+ * Unlike `account.picture` and `chests.entries.download`, this one CANNOT be
217
+ * asked for anonymously. The action resolves the ticket through
218
+ * `Ticket.viewable_by(Current.user)` and declares `oauth_scope
219
+ * "tickets:write"`, so a request with no credential is a 404 on someone
220
+ * else's ticket and a 401 on your own. The credential has to ride the first
221
+ * hop, which is exactly what makes this awkward.
222
+ *
223
+ * The awkward part. Rails answers `302` to `minio.omelhorsite.pt` and `fetch`
224
+ * follows it. Two different things then happen depending on how the client
225
+ * was built:
226
+ *
227
+ * - **token mode** (`credentials: "omit"`). The Fetch standard strips
228
+ * `Authorization` on a cross-origin redirect, so the store gets a clean
229
+ * presigned request. The hop also swaps the origin for an opaque one, so
230
+ * MinIO sees `Origin: null` and answers `Access-Control-Allow-Origin: *` -
231
+ * which an uncredentialed request accepts. This works, in a browser and
232
+ * out of one.
233
+ * - **cookie mode** (`sessionCookie: true`, the production web app). Same
234
+ * `Origin: null`, same `*` back, but now the request carries credentials,
235
+ * and wildcard plus credentials is illegal in CORS regardless of
236
+ * `Access-Control-Allow-Credentials`. The browser rejects the response and
237
+ * `fetch` rejects with an opaque "Failed to fetch".
238
+ *
239
+ * And it cannot be repaired the way the other two were, because the obvious
240
+ * repair does not exist in a browser: `redirect: "manual"` there yields an
241
+ * OPAQUE-REDIRECT response - status 0, empty header list - so `Location` is
242
+ * unreadable and the second hop cannot be re-issued without credentials.
243
+ * `redirect: "error"` and `XMLHttpRequest` are no better. Reading `Location`
244
+ * works only off-browser, where nothing was broken to begin with. The other
245
+ * two endpoints escape by needing no credential at all; this one has no such
246
+ * escape, and the backend exposes no `..._url` companion route the way
247
+ * `fs_nodes` does with `data_url`.
248
+ *
249
+ * So in a browser in cookie mode, use {@link attachmentUrl} and let the
250
+ * platform fetch the bytes - an `<img>`, a `<video>`, an `<a download>` make
251
+ * no-cors requests and follow the redirect with no CORS check at all. This
252
+ * method stays for every other context, and turns the browser failure into an
253
+ * error that says so instead of an unexplained network fault.
219
254
  *
220
255
  * Take `blobId` from {@link TicketAttachment.blob_id}; the filename and
221
256
  * content type are on the same record, which is why this hands back bare
222
257
  * bytes.
258
+ *
259
+ * @throws {OmsError} `unsupported` when the redirect was blocked by CORS,
260
+ * naming {@link attachmentUrl} as the way through.
261
+ * @throws {OmsApiError} 404 when the blob is not on that ticket, or the
262
+ * ticket is not yours.
223
263
  */
224
264
  attachment(id: TicketId, blobId: number, options?: RequestOptions): Promise<Blob>;
265
+ /**
266
+ * Absolute URL for one attachment, for an `<img>`, an `<a download>` or a new
267
+ * tab. In a browser in cookie mode this is the ONLY way to get at the bytes;
268
+ * see {@link attachment} for why.
269
+ *
270
+ * Asynchronous, unlike `account.pictureUrl` and
271
+ * `chests.entries.downloadUrl`, and the difference is not an accident: those
272
+ * two routes are anonymous, this one is not, so a credential has to be
273
+ * resolved before the URL means anything. Resolving it may involve a token
274
+ * refresh, which is why it cannot be a getter.
275
+ *
276
+ * How the URL authenticates depends on the client, matching what the backend
277
+ * accepts (`Session.candidate_tokens` reads the `Authorization` header, then
278
+ * `?token=`, then the `oms_session` cookie):
279
+ *
280
+ * - **cookie mode**: no credential in the URL. The browser attaches the
281
+ * `oms_session` cookie itself. It is host-only on the API host and
282
+ * `SameSite=Lax`, so this works from a page on `omelhorsite.pt` or another
283
+ * host under it, and from nowhere else. Do not add `crossorigin` to the
284
+ * element: it turns a no-cors load into a CORS one and re-creates the exact
285
+ * failure this method exists to avoid.
286
+ * - **token mode**: the token is appended as `?token=`. Rails accepts a query
287
+ * credential on API routes precisely because a native client has no cookie
288
+ * jar and an `<img>` cannot carry a header.
289
+ *
290
+ * THE TOKEN-MODE URL CONTAINS A LIVE CREDENTIAL. It goes into the DOM, into
291
+ * the server access log, and into anything that records URLs. Build it at the
292
+ * moment of use, never store it, never log it, and never put it somewhere
293
+ * another person can read - anyone holding it holds the session until it is
294
+ * revoked. When the URL is destined for something outside your own page,
295
+ * fetch the bytes with {@link attachment} and hand over a `blob:` URL
296
+ * instead.
297
+ *
298
+ * ```tsx
299
+ * const src = await oms.tickets.attachmentUrl(ticket.id, file.blob_id);
300
+ * <img src={src} alt={file.filename} />
301
+ * ```
302
+ */
303
+ attachmentUrl(id: TicketId, blobId: number): Promise<string>;
225
304
  }
226
305
  /**
227
306
  * 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;