@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
@@ -0,0 +1,534 @@
1
+ /** The `library.books` namespace, with the inputs, limits and upload helpers only it uses. */
2
+ import { Resource } from "../../http";
3
+ import type { ListParams } from "../../listing";
4
+ import type { FileInput, FileOutput, Id, NativeFile, Paginated, RequestOptions } from "../../types";
5
+ import type { Book, BookAnnotation, BookAnnotationLocation, BookFormat, BookId, BookVisibility, PublicLibrary } from "./types";
6
+ /**
7
+ * Ceiling `POST /books` enforces on the uploaded file: 100 MiB.
8
+ *
9
+ * NOT the ceiling you will actually hit in production. See
10
+ * {@link BOOK_UPLOAD_EDGE_LIMIT_BYTES} and {@link LibraryBooksNamespace.create}.
11
+ */
12
+ export declare const BOOK_MAX_FILE_BYTES = 104857600;
13
+ /**
14
+ * The ceiling that bites first: roughly 100 MB of request body, refused by
15
+ * Cloudflare before the origin is reached.
16
+ *
17
+ * Deliberately a decimal 100 MB against Rails' binary 100 MiB. The gap is real
18
+ * and it is exactly the band where the failure stops making sense: a 102 MB
19
+ * book is inside the limit Rails would allow and outside the one the edge
20
+ * allows, so it fails with a `413` whose body is Cloudflare's HTML error page
21
+ * and mentions neither books nor a size. There is no chunked upload route for
22
+ * books - `POST /books` is a single multipart request - so this ceiling is
23
+ * real and cannot be worked around by splitting the file.
24
+ */
25
+ export declare const BOOK_UPLOAD_EDGE_LIMIT_BYTES = 100000000;
26
+ /** Longest title accepted. Longer titles fail validation with a 400. */
27
+ export declare const BOOK_TITLE_MAX_LENGTH = 300;
28
+ /** Longest author accepted. */
29
+ export declare const BOOK_AUTHOR_MAX_LENGTH = 200;
30
+ /**
31
+ * Most tags a book keeps.
32
+ *
33
+ * The server does NOT reject a longer list: it strips, downcases,
34
+ * de-duplicates and then takes the first 30 in silence. Send more and the
35
+ * excess is gone with no error to notice.
36
+ */
37
+ export declare const BOOK_MAX_TAGS = 30;
38
+ /** Longest tag kept. Longer tags are TRUNCATED, not rejected. */
39
+ export declare const BOOK_TAG_MAX_LENGTH = 40;
40
+ /**
41
+ * Book-file reads an ANONYMOUS caller gets per hour, per IP, across
42
+ * `GET /books/:id/file` and `GET /books/:id/download` together.
43
+ *
44
+ * A signed-in caller is exempt. It exists because an anonymous visitor
45
+ * pulling book bytes is the one expensive thing sharing a book opens up.
46
+ */
47
+ export declare const ANONYMOUS_BOOK_FILE_RATE_LIMIT_PER_HOUR = 120;
48
+ /** Filter columns of `GET /books`, on top of {@link BASE_FILTER_COLUMNS}. */
49
+ export declare const BOOK_FILTER_COLUMNS: readonly ["title", "author", "format", "user_id"];
50
+ /** `extra_options` keys of `GET /books`. */
51
+ export declare const BOOK_EXTRA_OPTION_KEYS: readonly ["scope", "owner_handle", "tags"];
52
+ /** `extra_options` of `GET /books`. {@link ListBooksParams} has camelCased shortcuts for each key. */
53
+ export interface BookExtraOptions {
54
+ readonly scope?: "mine" | "explore";
55
+ readonly owner_handle?: string;
56
+ readonly tags?: readonly string[];
57
+ }
58
+ /** Shortcuts over the filter columns of `GET /books`. */
59
+ export interface BookFilters {
60
+ /** Partial, accent-insensitive match on the title (`search[title]`). */
61
+ readonly title?: string;
62
+ /** Partial, accent-insensitive match on the author (`search[author]`). */
63
+ readonly author?: string;
64
+ /** Exact match. Use it to split a shelf into PDFs and EPUBs. */
65
+ readonly format?: BookFormat;
66
+ /** Exact owner id. For a handle, use {@link ListBooksParams.ownerHandle}. */
67
+ readonly userId?: Id;
68
+ /** Exact ids. An array becomes `IN (...)`. */
69
+ readonly ids?: readonly BookId[];
70
+ }
71
+ /** Arguments for {@link LibraryBooksNamespace.list}. */
72
+ export interface ListBooksParams extends BookFilters, ListParams<(typeof BOOK_FILTER_COLUMNS)[number], BookExtraOptions> {
73
+ /**
74
+ * `"mine"` narrows to the caller's own books, `"explore"` to everybody
75
+ * else's. Omitted, the listing is own + all public, mixed together.
76
+ *
77
+ * `"explore"` is a no-op for an anonymous caller (there is no "else" to
78
+ * exclude), which is correct rather than a bug: an anonymous listing is
79
+ * already nothing but other people's public books.
80
+ */
81
+ readonly scope?: "mine" | "explore";
82
+ /**
83
+ * Narrow to one owner by handle, matched case-insensitively.
84
+ *
85
+ * An unknown handle returns an EMPTY list, not a 404. Do not read empty as
86
+ * "this person has no public books".
87
+ *
88
+ * Sending both this and `scope: "explore"` is contradictory when the handle
89
+ * is your own; {@link LibraryBooksNamespace.explore} drops the scope whenever
90
+ * a handle is given.
91
+ */
92
+ readonly ownerHandle?: string;
93
+ /**
94
+ * Books carrying ALL of these tags (a `jsonb @>` containment test, so it is
95
+ * AND, never OR). Stripped and downcased before matching, exactly as tags
96
+ * are stored.
97
+ */
98
+ readonly tags?: readonly string[];
99
+ /** `"column:asc"` / `"column:desc"`. See {@link LibraryBooksNamespace.list}. */
100
+ readonly order?: string;
101
+ /** Random ordering. Disables the `ETag`, and is not stable across pages. */
102
+ readonly random?: boolean;
103
+ }
104
+ /** Arguments for {@link LibraryBooksNamespace.create}. */
105
+ export interface CreateBookInput {
106
+ /**
107
+ * The book. Required, and the ONLY thing that decides the format: the server
108
+ * reads the extension, falls back to the content type, and refuses anything
109
+ * that is not a PDF or an EPUB.
110
+ *
111
+ * On React Native pass the picker's `{ uri, name, type }` object directly -
112
+ * it is appended verbatim and streamed off disk by the native layer. Make
113
+ * sure `name` carries the real extension, because a `uri` alone does not.
114
+ */
115
+ readonly file: FileInput | NativeFile;
116
+ /** Optional cover image. Re-encoded to a WebP thumbnail server-side. */
117
+ readonly cover?: FileInput | NativeFile;
118
+ /**
119
+ * Defaults to the FILENAME without its extension when omitted or blank.
120
+ * That is a real fallback, not an error path, so an untitled upload gets
121
+ * `"war-and-peace"` rather than being rejected.
122
+ */
123
+ readonly title?: string;
124
+ readonly author?: string;
125
+ readonly description?: string;
126
+ readonly isbn?: string;
127
+ readonly language?: string;
128
+ /** Coerced with `to_i` server-side, so `"abc"` becomes `0`, not an error. */
129
+ readonly pageCount?: number;
130
+ /** Stripped, downcased, de-duplicated, truncated, and cut to the first 30. */
131
+ readonly tags?: readonly string[];
132
+ }
133
+ /**
134
+ * Arguments for {@link LibraryBooksNamespace.update}.
135
+ *
136
+ * `null` CLEARS a column. It works in both encodings: as JSON it is a literal
137
+ * `null`, and as multipart it is written as the `\b` sentinel, which the
138
+ * server decodes back to a null for update params exactly as it does for
139
+ * filters. `undefined` leaves the column alone.
140
+ */
141
+ export interface UpdateBookInput {
142
+ readonly title?: string;
143
+ readonly author?: string | null;
144
+ readonly description?: string | null;
145
+ readonly isbn?: string | null;
146
+ readonly language?: string | null;
147
+ readonly pageCount?: number | null;
148
+ /** The one field that decides who can see the book. Not settable at create. */
149
+ readonly visibility?: BookVisibility;
150
+ /** Replaces the whole list. There is no add or remove. */
151
+ readonly tags?: readonly string[];
152
+ /**
153
+ * Replaces the cover. Its presence is what forces the request to multipart;
154
+ * every other field goes as JSON.
155
+ *
156
+ * There is no way to REMOVE a cover through this endpoint: the field is
157
+ * only read when present, and `cover: null` clears nothing.
158
+ */
159
+ readonly cover?: FileInput | NativeFile;
160
+ }
161
+ /** A byte range of a book file, as {@link LibraryBooksNamespace.fileRange} returns it. */
162
+ export interface BookFileRange {
163
+ /** The bytes. */
164
+ readonly data: ArrayBuffer;
165
+ /** First byte offset, inclusive, as the server reported it. */
166
+ readonly start: number;
167
+ /** Last byte offset, INCLUSIVE. HTTP ranges are closed at both ends. */
168
+ readonly end: number;
169
+ /** Total size of the file, from the `Content-Range` header. */
170
+ readonly total: number;
171
+ /** Pinned to the book's format server-side, never sniffed from the bytes. */
172
+ readonly contentType: string;
173
+ }
174
+ /** The `library.books` namespace, reachable as `oms.library.books`. */
175
+ export declare class LibraryBooksNamespace extends Resource {
176
+ /**
177
+ * `GET /books` - browse the library.
178
+ *
179
+ * WORKS ANONYMOUSLY, and what it returns depends on who is asking: every
180
+ * `public` book plus, for a signed-in caller, their own books whatever
181
+ * their visibility. `unlisted` books are absent by
182
+ * construction - that is what unlisted means - so a reader's own unlisted
183
+ * book DOES appear here (it is theirs) while somebody else's never will.
184
+ *
185
+ * Pagination is FORCED: a request with no page modifier gets `1:500`, and
186
+ * any larger size is clamped back to 500. That is a DoS guard - an
187
+ * unbounded listing holds a server thread and a database connection for as
188
+ * long as the serialisation takes.
189
+ * The SDK always sends a page modifier, so {@link Paginated.pageSize} always
190
+ * reports the size the rows were counted against.
191
+ *
192
+ * Order defaults to whatever Postgres returns, which is NOT stable across
193
+ * pages. Pass an explicit `order` for any paged walk; `"created_at:desc"` is
194
+ * what every library screen uses. An unknown column in `order` is IGNORED in
195
+ * silence, so a typo costs you the ordering and no error.
196
+ *
197
+ * The response carries an `ETag` and honours `If-None-Match` (except with
198
+ * `random`), so a repeated identical listing is cheap for the server even
199
+ * though the SDK does not cache it for you.
200
+ *
201
+ * @throws {OmsApiError} 400 naming the offending key when a filter is not
202
+ * one of `title`, `author`, `format`, `user_id`, `id`, `created_at`,
203
+ * `updated_at`. Filters fail closed on purpose: silently dropping an
204
+ * unknown one used to answer with the UNFILTERED set.
205
+ */
206
+ list(params?: ListBooksParams, options?: RequestOptions): Promise<Paginated<Book>>;
207
+ /**
208
+ * The caller's own books, whatever their visibility. Sugar for
209
+ * {@link list} with `scope: "mine"`.
210
+ *
211
+ * Needs a credential to mean anything: the scope narrows to the caller's own
212
+ * `user_id`, and for an anonymous caller that matches no row. An anonymous
213
+ * call therefore returns an EMPTY list rather than a 401 - the endpoint is
214
+ * genuinely public and has nothing to complain about.
215
+ */
216
+ mine(params?: Omit<ListBooksParams, "scope">, options?: RequestOptions): Promise<Paginated<Book>>;
217
+ /**
218
+ * Other people's public books. Sugar for {@link list} with
219
+ * `scope: "explore"`, or with `ownerHandle` when one is given.
220
+ *
221
+ * The scope is DROPPED as soon as a handle is supplied: asking for
222
+ * "everybody but me, and also only this person" reads as a filter but
223
+ * behaves as a way to make your own library invisible when you pass your
224
+ * own handle.
225
+ */
226
+ explore(params?: Omit<ListBooksParams, "scope">, options?: RequestOptions): Promise<Paginated<Book>>;
227
+ /**
228
+ * `GET /books/:id` - one book.
229
+ *
230
+ * The route an `unlisted` share link resolves through, so it works
231
+ * anonymously and its scope is deliberately WIDER than the listing's:
232
+ * public, unlisted, plus your own.
233
+ *
234
+ * Returns exactly what a row of {@link list} carries.
235
+ *
236
+ * @throws {OmsApiError} 404 `"Resource not found"` - for an id that does not
237
+ * exist AND for a private book belonging to someone else, indistinguishably.
238
+ * The scope is applied before the id is compared, which is what stops the
239
+ * endpoint being an existence oracle for other people's private books.
240
+ */
241
+ get(id: BookId, options?: RequestOptions): Promise<Book>;
242
+ /**
243
+ * `POST /books` - uploads a PDF or an EPUB and adds it to the library.
244
+ *
245
+ * Multipart, synchronous, and slow: Rails buffers the whole body, writes the
246
+ * file to storage, and derives a WebP thumbnail when a cover came with it.
247
+ * The per-attempt deadline defaults to five minutes here instead of the
248
+ * client's usual one, and the call is NOT retried - a replay after a lost
249
+ * answer uploads the book twice.
250
+ *
251
+ * ## Two size ceilings, and the one you will actually hit
252
+ *
253
+ * Rails refuses anything over {@link BOOK_MAX_FILE_BYTES} (100 MiB) with
254
+ * `413 "File too big (max 100 MB)"`. In production that message is nearly
255
+ * unreachable, because Cloudflare sits in front and refuses a request body
256
+ * over roughly {@link BOOK_UPLOAD_EDGE_LIMIT_BYTES} with its OWN `413`
257
+ * before the origin sees a byte. That one arrives as an
258
+ * {@link OmsApiError} whose body is an HTML error page and which mentions
259
+ * neither books nor a limit, so a caller matching on the message will not
260
+ * recognise it. Match on `status === 413` instead, and say "the file is too
261
+ * large" whatever the body claims.
262
+ *
263
+ * There is no chunked route for books, so splitting the file is not an
264
+ * option; a 400 MB scan has to be shrunk before it can be uploaded. (This is
265
+ * the same ceiling `songs.import` documents, hit far more often here: a
266
+ * scanned book is routinely bigger than an album.)
267
+ *
268
+ * This method checks the size LOCALLY first when the size is knowable - a
269
+ * `Blob`, a `Uint8Array`, or a picked file whose descriptor reported one -
270
+ * and throws before spending several minutes uploading something that will
271
+ * be refused. A `ReadableStream` reports nothing and is sent as-is.
272
+ *
273
+ * ## What the server decides for you
274
+ *
275
+ * - **Format**, from the filename extension, falling back to the content
276
+ * type. A `.pdf` that is really an EPUB is stored as a PDF and will not
277
+ * open; the reader pins `Content-Type` to this value and sends
278
+ * `X-Content-Type-Options: nosniff`, so nothing downstream corrects it.
279
+ * - **Title**, when you send none: the filename without its extension.
280
+ * - **Visibility**: always `private`. The field is never read at creation,
281
+ * so passing one changes nothing. Sharing a freshly uploaded book is
282
+ * always a second call to {@link update}.
283
+ * - **`file_size_bytes`**, from the stored node. Not from anything you send.
284
+ *
285
+ * @throws {TypeError} before any request when the file is over the edge
286
+ * ceiling and its size was knowable locally.
287
+ * @throws {OmsAuthError} 401 `"Session required"` when anonymous. Note it is
288
+ * checked in the action, so this is a 401 and not the 403 an OAuth token
289
+ * would get.
290
+ * @throws {OmsApiError} 413 for an oversized body (from Rails, or from the
291
+ * edge - see above); 400 `"No file provided"`, `"Unsupported file type,
292
+ * only PDF and EPUB are accepted"`, `"Library storage unavailable"`, or the
293
+ * model's validation messages.
294
+ */
295
+ create(input: CreateBookInput, options?: RequestOptions): Promise<Book>;
296
+ /**
297
+ * `PATCH /books/:id` - edits metadata, visibility, and optionally the cover.
298
+ *
299
+ * JSON normally; multipart as soon as {@link UpdateBookInput.cover} is
300
+ * present, because that is the only way to carry a file. Both encodings reach
301
+ * the same code path, and this method papers over the one place they would
302
+ * otherwise differ: clearing a column. Every multipart field is a string, so
303
+ * there is no `null` to send - the backend's `\b` sentinel is decoded for
304
+ * update params exactly as it is for filters, and this writes it for you when
305
+ * you pass `null`.
306
+ *
307
+ * This is where a book becomes shareable. `visibility` is the only field that
308
+ * changes who can reach it, and it cannot be set at upload time.
309
+ *
310
+ * Replacing the cover leaves the OLD cover nodes behind as orphans until the
311
+ * book itself is destroyed: a new node is created and the book pointed at
312
+ * it, and only the previous compressed twin is destroyed. Not a bug you can
313
+ * fix from here, but it is why re-cropping a cover repeatedly costs storage
314
+ * quota.
315
+ *
316
+ * @throws {OmsAuthError} 401 when the book is not yours - the ownership
317
+ * check answers 401, not 403.
318
+ * @throws {OmsApiError} 404 when the id is unknown or the book is someone
319
+ * else's PRIVATE one (the lookup is scoped first); 400 with the model's
320
+ * validation messages, e.g. a `visibility` outside
321
+ * {@link BOOK_VISIBILITIES} or a title over
322
+ * {@link BOOK_TITLE_MAX_LENGTH}.
323
+ */
324
+ update(id: BookId, input: UpdateBookInput, options?: RequestOptions): Promise<Book>;
325
+ /**
326
+ * `DELETE /books/:id` - removes the book, its file, its covers, its
327
+ * annotations and its place on every shelf.
328
+ *
329
+ * Genuinely destructive and not recoverable: the stored PDF or EPUB goes
330
+ * with the row, and so do every reader's highlights on it.
331
+ *
332
+ * @throws {OmsAuthError} 401 when the book is not yours.
333
+ * @throws {OmsApiError} 404 the second time, because the row is already gone.
334
+ * That is why this is never retried on a torn connection: a replay would
335
+ * report "not found" for a delete that worked perfectly well.
336
+ */
337
+ delete(id: BookId, options?: RequestOptions): Promise<void>;
338
+ /**
339
+ * `GET /books/libraries` - every reader who opted their library public and
340
+ * has at least one public book.
341
+ *
342
+ * A directory of PEOPLE, not of books - see {@link PublicLibrary}. Anonymous,
343
+ * unpaged, and cheap: the set is small by construction and the counts come
344
+ * from one grouped query.
345
+ *
346
+ * A reader appears only when BOTH halves are true: `library_public` on their
347
+ * user record (set through `account.update`, not from this namespace) AND at
348
+ * least one book at `public`. Making a single book public does not publish a
349
+ * library, and publishing a library with only unlisted books lists nobody.
350
+ */
351
+ libraries(options?: RequestOptions): Promise<PublicLibrary[]>;
352
+ /**
353
+ * `GET /books/:id/file` - the raw bytes, for an in-app reader.
354
+ *
355
+ * The one media route in this namespace that does NOT redirect: Rails serves
356
+ * the bytes itself, same-origin to the API, precisely so a reader
357
+ * (`react-pdf`, `epub.js`) can pull them over XHR without a cross-origin hop
358
+ * into object storage. It sends `Accept-Ranges: bytes`, pins `Content-Type`
359
+ * to the validated format, and sets `X-Content-Type-Options: nosniff` so a
360
+ * doctored `.pdf` full of HTML cannot be rendered as a document.
361
+ *
362
+ * Conditional GET is deliberately DISABLED on this route
363
+ * (`Cache-Control: no-cache, no-store, must-revalidate`): the body would
364
+ * otherwise carry an `ETag` and the browser would revalidate into an empty
365
+ * `304` that an XHR reader cannot parse. So every call is a full transfer
366
+ * and caching is the client's job.
367
+ *
368
+ * **This reads the whole book into memory twice** - once on the server and
369
+ * once here. For a large PDF prefer
370
+ * {@link fileRange}, which is what a PDF viewer does on its own, or
371
+ * {@link fileUrl} plus a native downloader on a phone.
372
+ *
373
+ * Anonymous callers are limited to
374
+ * {@link ANONYMOUS_BOOK_FILE_RATE_LIMIT_PER_HOUR} reads an hour per IP across
375
+ * this route and {@link download} together; a signed-in caller is not
376
+ * limited by it. The route is also exempt from the general 600/min ceiling,
377
+ * so a Range-reading viewer opening one book cannot trip it.
378
+ *
379
+ * @throws {OmsApiError} 404 `"No file attached"` when the row exists but its
380
+ * node lost its blob, and `"Resource not found"` when the book is not
381
+ * visible to the caller; 429 for an anonymous caller over the hourly cap.
382
+ */
383
+ file(id: BookId, options?: RequestOptions): Promise<FileOutput>;
384
+ /**
385
+ * `GET /books/:id/file` with a `Range` header - one slice of the book.
386
+ *
387
+ * This is how a PDF viewer opens a 60 MB scan in under a second: it reads the
388
+ * trailer, then the pages it needs, and never downloads the rest. The server
389
+ * answers `206` with `Content-Range` and reads only the slice, so the origin
390
+ * does not read the whole file either.
391
+ *
392
+ * `end` is INCLUSIVE, as HTTP ranges are: `{ start: 0, end: 1023 }` is the
393
+ * first 1024 bytes. Omit `end` for "from here to the end of the file". A
394
+ * suffix range (the LAST n bytes) is not offered here because the server's
395
+ * `Content-Range` is the only way back to absolute offsets and this method
396
+ * would have to guess; send the header yourself through `http.raw` if you
397
+ * need one.
398
+ *
399
+ * ONE range per request. The server answers `416` to a multi-range header
400
+ * rather than building a multipart/byteranges body, so do not batch.
401
+ *
402
+ * @throws {OmsApiError} 416 when the range starts past the end of the file,
403
+ * or when more than one range was asked for. The response carries
404
+ * a `Content-Range` naming the real size, with a star where the range
405
+ * would be. That is how a caller learns the size after guessing wrong.
406
+ * @throws {OmsError} when the server answered `200` instead of `206` - a
407
+ * proxy that stripped the `Range` header, which would otherwise be read as
408
+ * a slice and quietly corrupt the reader's buffer.
409
+ */
410
+ fileRange(id: BookId, range: {
411
+ start: number;
412
+ end?: number;
413
+ }, options?: RequestOptions): Promise<BookFileRange>;
414
+ /**
415
+ * Absolute URL of the book's bytes, for a reader that fetches them itself.
416
+ *
417
+ * Synchronous, and it carries NO CREDENTIAL - it cannot, because a token
418
+ * provider may be async and this is called while a component renders. What
419
+ * that means in practice depends on the book:
420
+ *
421
+ * - a `public` or `unlisted` book opens with no credential at all;
422
+ * - a `private` one needs one, and the caller has to attach it. For pdf.js
423
+ * that is `httpHeaders: { Authorization: 'Bearer ...' }` on a token client,
424
+ * or `withCredentials: true` on a cookie client. Both, on a client that has
425
+ * both, would be a request the API refuses to disambiguate.
426
+ *
427
+ * Prefer NOT to append `?token=...`. The API does accept a query token, but
428
+ * it puts a live session credential into access logs and into anything that
429
+ * copies the URL - a pasted link hands over the account, and the token never
430
+ * expires on its own.
431
+ *
432
+ * A query token is NOT counted as anonymous: a valid `?token=` resolves to a
433
+ * live session and the request is keyed to that session in the
434
+ * authenticated 600/min bucket. The leak is the reason to avoid it; the
435
+ * budget is not.
436
+ */
437
+ fileUrl(id: BookId): string;
438
+ /**
439
+ * `GET /books/:id/cover` - `302` to the cover image in object storage.
440
+ *
441
+ * Prefer {@link coverUrl} in markup. This method exists for when the bytes
442
+ * themselves are wanted, and it sends the caller's credential because it has
443
+ * to: the book is resolved through the caller's visibility, so your own
444
+ * private book's cover is a `404` without one.
445
+ *
446
+ * ## It does not work from a browser, and that is not fixable here
447
+ *
448
+ * The `302` goes to `minio.omelhorsite.pt`. Per the Fetch standard a CORS
449
+ * request redirected cross-origin gets an opaque origin on the next hop, MinIO
450
+ * answers a null origin with `Access-Control-Allow-Origin: *`, and a wildcard
451
+ * is illegal for a credentialed request - so the browser rejects the response
452
+ * before any JavaScript sees it. `account.picture` solves this by sending no
453
+ * credential; that is not available here, because a private cover needs one.
454
+ *
455
+ * In a browser, put {@link coverUrl} in an `<img>` and let the platform
456
+ * follow the redirect with no CORS check at all. In Bun, in a Worker and on
457
+ * React Native, where no CORS is enforced, this method is fine.
458
+ *
459
+ * @throws {OmsApiError} 404 `"No cover"` when the book has none, and
460
+ * `"Resource not found"` when it is not visible to the caller.
461
+ */
462
+ cover(id: BookId, options?: RequestOptions): Promise<FileOutput>;
463
+ /**
464
+ * Absolute URL of the cover, for an `<img>` or a `background-image`.
465
+ *
466
+ * Synchronous, uncredentialed, and safe to put in markup for a `public` or
467
+ * `unlisted` book. A PRIVATE book's cover answers `404` through this URL
468
+ * unless the platform attaches the session cookie for you - which an `<img>`
469
+ * on the same site does and an element with `crossorigin="anonymous"` does
470
+ * not. Give the element an `onError` that falls back to a generated cover.
471
+ *
472
+ * The redirect target is a presigned URL, so do not cache this URL's
473
+ * RESOLVED destination anywhere; cache this URL instead.
474
+ */
475
+ coverUrl(id: BookId): string;
476
+ /**
477
+ * `GET /books/:id/download` - `302` to the book file as an attachment.
478
+ *
479
+ * The difference from {@link file} is only the disposition: this one is
480
+ * `attachment` with a slugged filename (`war-and-peace.pdf`), and it goes
481
+ * through object storage rather than through Rails. Same CORS caveat as
482
+ * {@link cover} - see it - and the same anonymous ceiling of
483
+ * {@link ANONYMOUS_BOOK_FILE_RATE_LIMIT_PER_HOUR} per hour per IP.
484
+ *
485
+ * On a phone, do not pull a whole book through JavaScript for the sake of
486
+ * saving it: hand {@link downloadUrl} to a native downloader.
487
+ */
488
+ download(id: BookId, options?: RequestOptions): Promise<FileOutput>;
489
+ /**
490
+ * Absolute URL that saves the book as a file. Synchronous and
491
+ * uncredentialed, with the same visibility caveat as {@link coverUrl}.
492
+ *
493
+ * Suitable for an `<a href>` or for a native downloader. The filename comes
494
+ * from the server's `Content-Disposition`, so there is no need for a
495
+ * `download` attribute - which a cross-origin link ignores anyway.
496
+ */
497
+ downloadUrl(id: BookId): string;
498
+ /**
499
+ * `POST /books/:id/progress` - upserts "where I left off" for this reader.
500
+ *
501
+ * An UPSERT, not a create: there is exactly one progress marker per reader
502
+ * per book however many times this is called, and no id is needed. Call it
503
+ * freely as the reader turns pages.
504
+ *
505
+ * Private to the reader even on a public book, and it works on somebody
506
+ * else's public book - which is the point: reading a shared book should
507
+ * remember where you were.
508
+ *
509
+ * `location` is opaque `jsonb`. A blank or omitted one is stored as `{}`,
510
+ * which reads back as "no position recorded" rather than as page zero.
511
+ *
512
+ * Returns the annotation, so the row that comes back is the same shape
513
+ * {@link LibraryAnnotationsNamespace} deals in and can be dropped straight
514
+ * into a client-side cache of markers.
515
+ *
516
+ * @throws {OmsAuthError} 401 when anonymous - unlike the read routes, this
517
+ * one needs a session.
518
+ * @throws {OmsApiError} 404 when the book is not visible to the caller.
519
+ */
520
+ saveProgress(id: BookId, location: BookAnnotationLocation, options?: RequestOptions): Promise<BookAnnotation>;
521
+ /** JSON body for an update. `null` stays `null`; the transport does not touch a body. */
522
+ private updateBody;
523
+ /**
524
+ * Multipart fields for an update, with the one encoding difference fixed.
525
+ *
526
+ * `null` becomes the `\b` sentinel, which the server decodes back to a null
527
+ * for update params exactly as it does for filters - a form field has no
528
+ * other way to say "clear this column". An empty `tags` list is sent as a
529
+ * single empty string, because appending an empty array appends nothing and
530
+ * an absent key means "leave the tags alone", which is the opposite of what
531
+ * clearing them means. The server drops the blank entry, leaving `[]`.
532
+ */
533
+ private multipartUpdateFields;
534
+ }