@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
@@ -0,0 +1,1435 @@
1
+ /**
2
+ * The `library` namespace: the virtual library - books, shelves, private
3
+ * annotations, the public-library directory, and the streaming study assistant.
4
+ *
5
+ * Four endpoint families and one streaming route live here because they are one
6
+ * product: a reader uploads a PDF or an EPUB, arranges books onto shelves,
7
+ * highlights and annotates as they read, and asks an assistant about the
8
+ * passage in front of them. They are exposed as one entry class with four
9
+ * sub-namespaces ({@link LibraryNamespace.books}, `.shelves`, `.annotations`,
10
+ * `.chat`), and every sub-namespace is also exported on its own so a host that
11
+ * prefers `oms.books` can mount it there instead.
12
+ *
13
+ * ## Six things that have already cost bugs
14
+ *
15
+ * 1. **Every id here is an INTEGER.** `books`, `book_shelves`,
16
+ * `book_shelf_items` and `book_annotations` all kept auto-increment primary
17
+ * keys, so they arrive as JSON numbers - while the `user_id` and the
18
+ * `*_fs_node_id` columns right next to them are STRINGS. Do not widen
19
+ * {@link BookId} to `Id`.
20
+ * 2. **`private` / `unlisted` / `public` is a two-axis rule, not three levels.**
21
+ * `unlisted` means "reachable by whoever has the link, listed nowhere": it
22
+ * is returned by `GET /books/:id` and it is deliberately absent from
23
+ * `GET /books`. A client that builds its library screen from the listing and
24
+ * then wonders where an unlisted book went is reading the design correctly.
25
+ * 3. **Visibility cannot be set at creation.** `POST /books` does not read the
26
+ * field at all (`BookServices::Creator` never looks at it) and the column
27
+ * defaults to `private`. Sharing is always a second call. See
28
+ * {@link LibraryBooksNamespace.create}.
29
+ * 4. **The 100 MB upload ceiling is enforced twice, and the outer one wins.**
30
+ * Rails refuses a book over {@link BOOK_MAX_FILE_BYTES} with a `413` that
31
+ * says so; production sits behind Cloudflare, which refuses a request body
32
+ * over its own ~100 MB with a `413` of its own, before Rails is reached and
33
+ * without a word about books. See {@link LibraryBooksNamespace.create}.
34
+ * 5. **`GET /books/:id/cover` and `/download` answer `302` into object
35
+ * storage.** Following that redirect from a browser with a credential
36
+ * attached is rejected by CORS - the same failure `account.picture`
37
+ * documents. `/file` does NOT redirect: it is served by Rails, supports HTTP
38
+ * Range, and is the one to fetch bytes from.
39
+ * 6. **The chat answers `200` and can still fail.** The status is written
40
+ * before the model has produced a token, so a failure mid-generation arrives
41
+ * as an SSE `error` frame inside a successful response. See
42
+ * {@link BookChatNamespace.stream} and {@link BookChatError}.
43
+ *
44
+ * ## Rate ceilings that apply here and nowhere else
45
+ *
46
+ * - `POST /books/:id/chat`: **15 per minute**, keyed on the `Authorization`
47
+ * header or, with none, on the client IP (see {@link BOOK_CHAT_RATE_LIMIT_PER_MINUTE}).
48
+ * - `GET /books/:id/file` and `/download`, **anonymous callers only**: **120
49
+ * per hour per IP** (see {@link ANONYMOUS_BOOK_FILE_RATE_LIMIT_PER_HOUR}).
50
+ * A signed-in owner is not limited by it.
51
+ * - `GET /books/:id/file` is additionally EXEMPT from the general
52
+ * authenticated ceiling of 600/min, because a Range-reading PDF viewer
53
+ * legitimately makes dozens of requests to open one book.
54
+ *
55
+ * Everything else in this namespace sits under the general ceiling: 600/min
56
+ * authenticated, 120/min per IP anonymous.
57
+ *
58
+ * ## No OAuth token reaches ANY of this
59
+ *
60
+ * None of the four controllers declares an `oauth_scope`, and `enforce_oauth_scope!`
61
+ * denies by omission - so every endpoint here answers `403` to a Doorkeeper
62
+ * access token, INCLUDING the ones that are otherwise open to the public.
63
+ * That last part is the trap: `GET /books` works for a caller with no
64
+ * credential at all and fails for the same caller once it attaches an OAuth
65
+ * token, because the scope filter runs even where `allow_unauthenticated_access`
66
+ * took the auth filter off. A session (cookie or bearer session token) is the
67
+ * only credential this namespace accepts. An OAuth-backed integration that
68
+ * wants public books has to send nothing.
69
+ */
70
+ import { OmsError } from "../errors";
71
+ import { Resource } from "../http";
72
+ import type { FileInput, FileOutput, Id, NativeFile, PageParams, Paginated, QueryValue, RequestOptions, Timestamp } from "../types";
73
+ /**
74
+ * Primary key of a book. A NUMBER: `books` never moved to the opaque string
75
+ * ids the account-side tables use, even though its `user_id` and its three
76
+ * `*_fs_node_id` columns are strings.
77
+ */
78
+ export type BookId = number;
79
+ /** Primary key of a shelf. A number, like every id in this file. */
80
+ export type BookShelfId = number;
81
+ /** Primary key of an annotation. A number. */
82
+ export type BookAnnotationId = number;
83
+ /** The two file formats the library accepts. Mirrors `Book::FORMATS`. */
84
+ export declare const BOOK_FORMATS: readonly ["pdf", "epub"];
85
+ /** One of {@link BOOK_FORMATS}. */
86
+ export type BookFormat = (typeof BOOK_FORMATS)[number];
87
+ /**
88
+ * Visibility of a book or a shelf. Mirrors `Book::VISIBILITIES`, which
89
+ * `BookShelf::VISIBILITIES` aliases, so the two can never drift apart.
90
+ *
91
+ * - `private` - owner only.
92
+ * - `unlisted` - anyone holding the link. Reachable by `GET /books/:id`,
93
+ * absent from every listing. This is the level a "share" button sets.
94
+ * - `public` - anyone, AND listed in the owner's library and in explore.
95
+ */
96
+ export declare const BOOK_VISIBILITIES: readonly ["private", "unlisted", "public"];
97
+ /** One of {@link BOOK_VISIBILITIES}. */
98
+ export type BookVisibility = (typeof BOOK_VISIBILITIES)[number];
99
+ /** Kinds of annotation. Mirrors `BookAnnotation::KINDS`. */
100
+ export declare const BOOK_ANNOTATION_KINDS: readonly ["highlight", "note", "bookmark", "progress"];
101
+ /** One of {@link BOOK_ANNOTATION_KINDS}. */
102
+ export type BookAnnotationKind = (typeof BOOK_ANNOTATION_KINDS)[number];
103
+ /**
104
+ * Ceiling `POST /books` enforces on the uploaded file: 100 MiB
105
+ * (`BooksController::MAX_FILE_BYTES`, `100.megabytes`).
106
+ *
107
+ * NOT the ceiling you will actually hit in production. See
108
+ * {@link BOOK_UPLOAD_EDGE_LIMIT_BYTES} and {@link LibraryBooksNamespace.create}.
109
+ */
110
+ export declare const BOOK_MAX_FILE_BYTES = 104857600;
111
+ /**
112
+ * The ceiling that bites first: roughly 100 MB of request body, refused by
113
+ * Cloudflare before the origin is reached.
114
+ *
115
+ * Deliberately a decimal 100 MB against Rails' binary 100 MiB. The gap is real
116
+ * and it is exactly the band where the failure stops making sense: a 102 MB
117
+ * book is inside the limit Rails would allow and outside the one the edge
118
+ * allows, so it fails with a `413` whose body is Cloudflare's HTML error page
119
+ * and mentions neither books nor a size. There is no chunked upload route for
120
+ * books - `POST /books` is a single multipart request - so this ceiling is
121
+ * real and cannot be worked around by splitting the file.
122
+ */
123
+ export declare const BOOK_UPLOAD_EDGE_LIMIT_BYTES = 100000000;
124
+ /** Mirrors `Book::TITLE_MAX_LENGTH`. Longer titles fail validation with a 400. */
125
+ export declare const BOOK_TITLE_MAX_LENGTH = 300;
126
+ /** Mirrors `Book::AUTHOR_MAX_LENGTH`. */
127
+ export declare const BOOK_AUTHOR_MAX_LENGTH = 200;
128
+ /**
129
+ * Most tags a book keeps. Mirrors `Book::MAX_TAGS`.
130
+ *
131
+ * The server does NOT reject a longer list: `normalize_tags` strips, downcases,
132
+ * de-duplicates and then takes the first 30 in silence. Send more and the
133
+ * excess is gone with no error to notice.
134
+ */
135
+ export declare const BOOK_MAX_TAGS = 30;
136
+ /** Mirrors `Book::TAG_MAX_LENGTH`. Longer tags are TRUNCATED, not rejected. */
137
+ export declare const BOOK_TAG_MAX_LENGTH = 40;
138
+ /** Mirrors `BookShelf::NAME_MAX_LENGTH`. */
139
+ export declare const BOOK_SHELF_NAME_MAX_LENGTH = 120;
140
+ /** Mirrors `BookShelf::DESCRIPTION_MAX_LENGTH`. */
141
+ export declare const BOOK_SHELF_DESCRIPTION_MAX_LENGTH = 1000;
142
+ /**
143
+ * Most shelves one user may own. Mirrors `BookShelf::MAX_PER_USER`.
144
+ *
145
+ * Validated `on: :create` only, so the 101st shelf fails with a `400` carrying
146
+ * `"You have reached the maximum of 100 shelves"`; existing shelves keep saving.
147
+ */
148
+ export declare const BOOK_SHELVES_MAX_PER_USER = 100;
149
+ /** Mirrors `BookAnnotation::NOTE_MAX_LENGTH`. */
150
+ export declare const BOOK_ANNOTATION_NOTE_MAX_LENGTH = 5000;
151
+ /** Mirrors `BookAnnotation::SELECTED_TEXT_MAX_LENGTH`. */
152
+ export declare const BOOK_ANNOTATION_SELECTED_TEXT_MAX_LENGTH = 8000;
153
+ /**
154
+ * Requests per minute allowed against `POST /books/:id/chat`.
155
+ *
156
+ * Keyed on the `Authorization` header when there is one, and on the client IP
157
+ * when there is not - so a cookie-authenticated web client shares one budget
158
+ * with every other visitor behind the same address. Over it: `429`.
159
+ */
160
+ export declare const BOOK_CHAT_RATE_LIMIT_PER_MINUTE = 15;
161
+ /**
162
+ * Largest chat request body Rails agrees to parse:
163
+ * `BookChatsController::MAX_BODY_BYTES`, 512 KiB. Over it, `413`.
164
+ *
165
+ * This is a bound on what is PARSED, not on what reaches the model - the model
166
+ * caps below are applied afterwards, inside `Ai::BookTutor`.
167
+ */
168
+ export declare const BOOK_CHAT_MAX_BODY_BYTES = 524288;
169
+ /**
170
+ * Characters of {@link BookChatContext.text} the assistant will read. Mirrors
171
+ * `Ai::BookTutor::MAX_CONTEXT_CHARS`.
172
+ *
173
+ * Silently truncated server-side, not refused. Sending a whole chapter costs
174
+ * the bytes and buys nothing past this point.
175
+ */
176
+ export declare const BOOK_CHAT_MAX_CONTEXT_CHARS = 12000;
177
+ /** Turns of history the assistant reads, newest kept. Mirrors `MAX_HISTORY_MESSAGES`. */
178
+ export declare const BOOK_CHAT_MAX_HISTORY_MESSAGES = 12;
179
+ /** Characters kept per history turn. Mirrors `MAX_MESSAGE_CHARS`. */
180
+ export declare const BOOK_CHAT_MAX_MESSAGE_CHARS = 4000;
181
+ /** Characters kept across the whole history. Mirrors `MAX_HISTORY_CHARS`. */
182
+ export declare const BOOK_CHAT_MAX_HISTORY_CHARS = 8000;
183
+ /**
184
+ * Book-file reads an ANONYMOUS caller gets per hour, per IP, across
185
+ * `GET /books/:id/file` and `GET /books/:id/download` together.
186
+ *
187
+ * A signed-in caller is exempt: the guard carries `if: -> { Current.user.nil? }`.
188
+ * It exists because an anonymous visitor pulling book bytes is the one
189
+ * expensive thing sharing a book opens up.
190
+ */
191
+ export declare const ANONYMOUS_BOOK_FILE_RATE_LIMIT_PER_HOUR = 120;
192
+ /**
193
+ * A book.
194
+ *
195
+ * `editable` is computed against the ASKING user, which is what lets one
196
+ * listing mix your own books with strangers' public ones and still know which
197
+ * ones offer an edit affordance. It is `updatable_by?`, so it is also the
198
+ * answer for "may I delete this".
199
+ */
200
+ export interface Book {
201
+ /** Integer primary key. See {@link BookId}. */
202
+ readonly id: BookId;
203
+ readonly created_at: Timestamp;
204
+ readonly updated_at: Timestamp;
205
+ readonly title: string;
206
+ readonly author: string | null;
207
+ readonly description: string | null;
208
+ /** Detected at upload from the extension or the content type, never from the caller. */
209
+ readonly format: BookFormat;
210
+ readonly isbn: string | null;
211
+ readonly language: string | null;
212
+ readonly page_count: number | null;
213
+ /** Size of the stored file. `null` only on rows that predate the column. */
214
+ readonly file_size_bytes: number | null;
215
+ /** Always an array, never `null`: the column is `jsonb NOT NULL DEFAULT '[]'`. */
216
+ readonly tags: string[];
217
+ readonly visibility: BookVisibility;
218
+ /** Owner. A string id, next to an integer `id`. */
219
+ readonly user_id: Id;
220
+ /** Storage node of the cover the owner uploaded. A STRING id. */
221
+ readonly cover_fs_node_id: Id | null;
222
+ /** Storage node of the WebP thumbnail the server derived from it. A STRING id. */
223
+ readonly compressed_cover_fs_node_id: Id | null;
224
+ /** True when either cover node is set. Branch on this, not on the two ids. */
225
+ readonly has_cover: boolean;
226
+ readonly owner_handle: string | null;
227
+ readonly owner_name: string | null;
228
+ /** `updatable_by?` for the ASKING user. Also answers "may I delete this". */
229
+ readonly editable: boolean;
230
+ }
231
+ /**
232
+ * One row of the public-library directory: a person, not a book.
233
+ *
234
+ * This is the only payload in the namespace that is NOT a Blueprinter record -
235
+ * `BooksController#libraries` builds the hashes by hand - so it carries no
236
+ * `id`, no `created_at` and no `updated_at`, and the owner is addressed by
237
+ * `handle`. Feed that handle back as {@link ListBooksParams.ownerHandle}.
238
+ */
239
+ export interface PublicLibrary {
240
+ readonly handle: string;
241
+ readonly name: string;
242
+ /** What the owner named their library, or `null` if they never did. */
243
+ readonly library_name: string | null;
244
+ readonly library_description: string | null;
245
+ /** PUBLIC books only. An owner's unlisted and private books are not counted. */
246
+ readonly book_count: number;
247
+ }
248
+ /**
249
+ * A shelf: a named, ordered group of one reader's own books.
250
+ *
251
+ * `books` is present ONLY on the `:extended` view, which means
252
+ * {@link LibraryShelvesNamespace.get} and the three arrange calls carry it and
253
+ * {@link LibraryShelvesNamespace.list} does not. That is not an oversight to
254
+ * work around by listing and hoping - it is why `get` exists.
255
+ */
256
+ export interface BookShelf {
257
+ readonly id: BookShelfId;
258
+ readonly created_at: Timestamp;
259
+ readonly updated_at: Timestamp;
260
+ readonly name: string;
261
+ readonly description: string | null;
262
+ readonly visibility: BookVisibility;
263
+ /** The owner's ordering of their own shelves. Ascending. */
264
+ readonly position: number;
265
+ /** Owner. A string id. */
266
+ readonly user_id: Id;
267
+ readonly owner_handle: string | null;
268
+ readonly owner_name: string | null;
269
+ /** `updatable_by?` for the ASKING user. */
270
+ readonly editable: boolean;
271
+ /**
272
+ * How many of this shelf's books the ASKING user may actually open, which is
273
+ * not how many are on it: a shelf can hold a book its owner later made
274
+ * private again, and counting those would promise a stranger rows they
275
+ * cannot see. So `book_count` can be smaller than `books.length` would be
276
+ * for the owner, and it costs one extra query PER SHELF in a listing.
277
+ */
278
+ readonly book_count: number;
279
+ /**
280
+ * The shelf's books, in `position` order, filtered to what the asking user
281
+ * may see. Present on the `:extended` view only - see the note above.
282
+ */
283
+ readonly books?: Book[];
284
+ }
285
+ /**
286
+ * Where in the book an annotation sits. Format-specific and opaque to the API:
287
+ * the column is `jsonb NOT NULL DEFAULT '{}'` and nothing server-side reads
288
+ * inside it.
289
+ *
290
+ * The two shapes the readers agree on today:
291
+ * - EPUB: `{ cfiRange }` for a range, `{ cfi }` for a point;
292
+ * - PDF: `{ pageNumber, rects: [{ x, y, w, h }] }`, page-normalised to `[0,1]`
293
+ * so a highlight survives a zoom.
294
+ *
295
+ * A new reader may write a new shape. Read defensively.
296
+ */
297
+ export type BookAnnotationLocation = Record<string, unknown>;
298
+ /**
299
+ * A highlight, a note, a bookmark, or a reading-progress marker.
300
+ *
301
+ * ALWAYS PRIVATE to the reader who made it, even on a public book:
302
+ * `BookAnnotation.viewable_by` is `none` for an anonymous caller and
303
+ * `where(user_id: user.id)` otherwise. There is no sharing of highlights, and
304
+ * `GET /book_annotations` therefore needs no user filter - it can only ever
305
+ * return your own.
306
+ */
307
+ export interface BookAnnotation {
308
+ readonly id: BookAnnotationId;
309
+ readonly created_at: Timestamp;
310
+ readonly updated_at: Timestamp;
311
+ readonly book_id: BookId;
312
+ /** Author. Always the caller, since nobody else's are visible. */
313
+ readonly user_id: Id;
314
+ readonly kind: BookAnnotationKind;
315
+ readonly location: BookAnnotationLocation;
316
+ /** Free-form, usually a CSS colour. Not validated server-side. */
317
+ readonly color: string | null;
318
+ readonly note: string | null;
319
+ /** The passage the reader selected, kept so a highlight can be listed without opening the book. */
320
+ readonly selected_text: string | null;
321
+ }
322
+ /** One turn of the conversation, as the client remembers it. */
323
+ export interface BookChatTurn {
324
+ /**
325
+ * Only `"user"` and `"assistant"` reach the model: `Ai::BookTutor::ALLOWED_ROLES`
326
+ * drops everything else, precisely so a client cannot inject a `"system"`
327
+ * turn into our own framing.
328
+ */
329
+ readonly role: "user" | "assistant";
330
+ readonly content: string;
331
+ }
332
+ /**
333
+ * The passage the question is about. The CLIENT decides what that is - the
334
+ * current selection, the chapter on screen, or whatever its search index
335
+ * retrieved - and the server only frames it.
336
+ */
337
+ export interface BookChatContext {
338
+ readonly kind: "selection" | "chapter" | "passages";
339
+ /** Chapter or section title, for the framing line. Truncated to 200 chars. */
340
+ readonly title?: string;
341
+ /** Truncated to {@link BOOK_CHAT_MAX_CONTEXT_CHARS} server-side, in silence. */
342
+ readonly text: string;
343
+ }
344
+ /** Arguments for {@link BookChatNamespace.stream} and {@link BookChatNamespace.ask}. */
345
+ export interface BookChatInput {
346
+ /**
347
+ * The whole conversation, oldest first, INCLUDING the question being asked.
348
+ * There is no server-side session: what you do not send did not happen.
349
+ */
350
+ readonly messages: readonly BookChatTurn[];
351
+ /** The passage in front of the reader. Omit it and the assistant is told so. */
352
+ readonly context?: BookChatContext | null;
353
+ }
354
+ /**
355
+ * One decoded SSE frame from the chat stream.
356
+ *
357
+ * Exactly one field is set per frame. `delta` carries text to append, `done`
358
+ * marks a complete answer, and `error` is a failure that happened AFTER the
359
+ * `200` was written.
360
+ */
361
+ export interface BookChatEvent {
362
+ readonly delta?: string;
363
+ readonly done?: boolean;
364
+ readonly error?: string;
365
+ }
366
+ /** Why a chat stream failed. See {@link BookChatError}. */
367
+ export type BookChatFailureReason =
368
+ /** The server wrote an `error` frame: generation failed after the 200. */
369
+ "assistant_unavailable"
370
+ /** The stream ended without a `done` frame, so the answer is cut short. */
371
+ | "truncated";
372
+ /**
373
+ * The chat stream failed AFTER the response status was written.
374
+ *
375
+ * This is not an {@link OmsApiError}, and that is the whole point of it
376
+ * existing. `BookChatsController` writes `200` and the SSE headers before the
377
+ * model has produced a single token, so anything that goes wrong from then on -
378
+ * the inference sidecar erroring, the proxy dropping a stream that went quiet -
379
+ * arrives INSIDE a successful response. Code that decides success by status
380
+ * alone reports a truncated answer, or an empty one, as a complete reply.
381
+ *
382
+ * `code` is `"server_error"`, so `error.retryable` is `true`: asking again is
383
+ * the right move for both reasons, subject to
384
+ * {@link BOOK_CHAT_RATE_LIMIT_PER_MINUTE}.
385
+ */
386
+ export declare class BookChatError extends OmsError {
387
+ static readonly errorName: string;
388
+ /** Which of the two post-200 failures this is. */
389
+ readonly reason: BookChatFailureReason;
390
+ /** Text already yielded before the failure. Usually worth showing, marked as incomplete. */
391
+ readonly partial: string;
392
+ constructor(message: string, reason: BookChatFailureReason, partial: string, url?: string);
393
+ toJSON(): Record<string, unknown>;
394
+ }
395
+ /**
396
+ * True when this error is the assistant refusing because every generation slot
397
+ * is taken.
398
+ *
399
+ * `Ai::StreamSlots.acquire` fails closed rather than queueing - a caller parked
400
+ * waiting for a slot is holding the very Puma thread the slots exist to protect
401
+ * - so the answer is `503` with `"The assistant is busy, try again in a moment"`.
402
+ * It is transient and it is NOT a rate limit: the caller did nothing wrong and
403
+ * `Retry-After` is not sent, so back off on your own and try again.
404
+ */
405
+ export declare function isBookChatBusy(error: unknown): boolean;
406
+ /**
407
+ * Whether {@link BookChatNamespace.stream} will deliver this answer in pieces
408
+ * on THIS runtime.
409
+ *
410
+ * `true` in a browser and in Bun, `false` in React Native, whose `fetch` hands
411
+ * the whole body over at the end and has no `ReadableStream` at all. Both
412
+ * paths reach the same endpoint and produce the same final text; only the
413
+ * arrival differs, and this is the question to ask before a UI promises a
414
+ * typing indicator. A typing indicator that never types is worse than a
415
+ * spinner that admits it is waiting.
416
+ *
417
+ * Read it at the point of use, never cached at module load: a host may install
418
+ * a polyfill after the SDK is imported.
419
+ */
420
+ export declare function bookChatIsIncremental(): boolean;
421
+ /** Filters `GET /books` accepts. Anything else is a `400` naming the key. */
422
+ export interface BookFilters {
423
+ /** Partial, accent-insensitive match on the title (`search[title]`). */
424
+ readonly title?: string;
425
+ /** Partial, accent-insensitive match on the author (`search[author]`). */
426
+ readonly author?: string;
427
+ /** Exact match. Use it to split a shelf into PDFs and EPUBs. */
428
+ readonly format?: BookFormat;
429
+ /** Exact owner id. For a handle, use {@link ListBooksParams.ownerHandle}. */
430
+ readonly userId?: Id;
431
+ /** Exact ids. An array becomes `IN (...)`. */
432
+ readonly ids?: readonly BookId[];
433
+ /** Escape hatch: extra `search[...]` keys. Unknown keys are a 400. */
434
+ readonly search?: Record<string, QueryValue>;
435
+ /** Escape hatch: extra `exact_search[...]` keys. Unknown keys are a 400. */
436
+ readonly exactSearch?: Record<string, QueryValue>;
437
+ }
438
+ /** Arguments for {@link LibraryBooksNamespace.list}. */
439
+ export interface ListBooksParams extends BookFilters, PageParams {
440
+ /**
441
+ * `"mine"` narrows to the caller's own books, `"explore"` to everybody
442
+ * else's. Omitted, the listing is own + all public, mixed together.
443
+ *
444
+ * `"explore"` is a no-op for an anonymous caller (there is no "else" to
445
+ * exclude), which is correct rather than a bug: an anonymous listing is
446
+ * already nothing but other people's public books.
447
+ */
448
+ readonly scope?: "mine" | "explore";
449
+ /**
450
+ * Narrow to one owner by handle, matched case-insensitively.
451
+ *
452
+ * An unknown handle returns an EMPTY list, not a 404 - `apply_owner` falls
453
+ * back to `.none`. Do not read empty as "this person has no public books".
454
+ *
455
+ * Sending both this and `scope: "explore"` is contradictory when the handle
456
+ * is your own; the frontend drops the scope whenever a handle is given, and
457
+ * so does {@link LibraryBooksNamespace.explore}.
458
+ */
459
+ readonly ownerHandle?: string;
460
+ /**
461
+ * Books carrying ALL of these tags (a `jsonb @>` containment test, so it is
462
+ * AND, never OR). Stripped and downcased before matching, exactly as tags
463
+ * are stored.
464
+ */
465
+ readonly tags?: readonly string[];
466
+ /** `"column:asc"` / `"column:desc"`. See {@link LibraryBooksNamespace.list}. */
467
+ readonly order?: string;
468
+ /** Random ordering. Disables the `ETag`, and is not stable across pages. */
469
+ readonly random?: boolean;
470
+ }
471
+ /** Arguments for {@link LibraryBooksNamespace.create}. */
472
+ export interface CreateBookInput {
473
+ /**
474
+ * The book. Required, and the ONLY thing that decides the format: the server
475
+ * reads the extension, falls back to the content type, and refuses anything
476
+ * that is not a PDF or an EPUB.
477
+ *
478
+ * On React Native pass the picker's `{ uri, name, type }` object directly -
479
+ * it is appended verbatim and streamed off disk by the native layer. Make
480
+ * sure `name` carries the real extension, because a `uri` alone does not.
481
+ */
482
+ readonly file: FileInput | NativeFile;
483
+ /** Optional cover image. Re-encoded to a WebP thumbnail server-side. */
484
+ readonly cover?: FileInput | NativeFile;
485
+ /**
486
+ * Defaults to the FILENAME without its extension when omitted or blank.
487
+ * That is a real fallback, not an error path, so an untitled upload gets
488
+ * `"war-and-peace"` rather than being rejected.
489
+ */
490
+ readonly title?: string;
491
+ readonly author?: string;
492
+ readonly description?: string;
493
+ readonly isbn?: string;
494
+ readonly language?: string;
495
+ /** Coerced with `to_i` server-side, so `"abc"` becomes `0`, not an error. */
496
+ readonly pageCount?: number;
497
+ /** Stripped, downcased, de-duplicated, truncated, and cut to the first 30. */
498
+ readonly tags?: readonly string[];
499
+ }
500
+ /**
501
+ * Arguments for {@link LibraryBooksNamespace.update}.
502
+ *
503
+ * `null` CLEARS a column. It works in both encodings: as JSON it is a literal
504
+ * `null`, and as multipart it is written as the backend's `\b` sentinel, which
505
+ * `CrudActions` decodes back to `nil` for update params exactly as it does for
506
+ * filters. `undefined` leaves the column alone.
507
+ */
508
+ export interface UpdateBookInput {
509
+ readonly title?: string;
510
+ readonly author?: string | null;
511
+ readonly description?: string | null;
512
+ readonly isbn?: string | null;
513
+ readonly language?: string | null;
514
+ readonly pageCount?: number | null;
515
+ /** The one field that decides who can see the book. Not settable at create. */
516
+ readonly visibility?: BookVisibility;
517
+ /** Replaces the whole list. There is no add or remove. */
518
+ readonly tags?: readonly string[];
519
+ /**
520
+ * Replaces the cover. Its presence is what forces the request to multipart;
521
+ * every other field goes as JSON.
522
+ *
523
+ * There is no way to REMOVE a cover through this endpoint: `params[:cover]`
524
+ * is only read when present, and `cover: null` clears nothing.
525
+ */
526
+ readonly cover?: FileInput | NativeFile;
527
+ }
528
+ /** Arguments for {@link LibraryShelvesNamespace.list}. */
529
+ export interface ListBookShelvesParams extends PageParams {
530
+ /** Exact owner id. The only way to ask for one person's shelves. */
531
+ readonly userId?: Id;
532
+ /** Exact visibility. */
533
+ readonly visibility?: BookVisibility;
534
+ /** Exact ids. An array becomes `IN (...)`. */
535
+ readonly ids?: readonly BookShelfId[];
536
+ /** `"column:asc"` / `"column:desc"`. Defaults to the endpoint's own ordering. */
537
+ readonly order?: string;
538
+ /** Escape hatch: extra `search[...]` keys. Unknown keys are a 400. */
539
+ readonly search?: Record<string, QueryValue>;
540
+ /** Escape hatch: extra `exact_search[...]` keys. Unknown keys are a 400. */
541
+ readonly exactSearch?: Record<string, QueryValue>;
542
+ }
543
+ /** Arguments for {@link LibraryShelvesNamespace.create}. */
544
+ export interface CreateBookShelfInput {
545
+ readonly name: string;
546
+ readonly description?: string | null;
547
+ /** Defaults to `"private"` server-side. */
548
+ readonly visibility?: BookVisibility;
549
+ }
550
+ /**
551
+ * Arguments for {@link LibraryShelvesNamespace.update}.
552
+ *
553
+ * `position` is writable here and nowhere else: it orders the owner's SHELVES
554
+ * against each other, and has nothing to do with the order of books ON a shelf
555
+ * (that is {@link LibraryShelvesNamespace.reorder}).
556
+ */
557
+ export interface UpdateBookShelfInput {
558
+ readonly name?: string;
559
+ readonly description?: string | null;
560
+ readonly visibility?: BookVisibility;
561
+ readonly position?: number;
562
+ }
563
+ /** Arguments for {@link LibraryAnnotationsNamespace.list}. */
564
+ export interface ListBookAnnotationsParams extends PageParams {
565
+ /** Exact book id. The filter every reader screen uses. */
566
+ readonly bookId?: BookId;
567
+ /** Exact kind, or an array of kinds (which becomes `IN (...)`). */
568
+ readonly kind?: BookAnnotationKind | readonly BookAnnotationKind[];
569
+ /** `"column:asc"` / `"column:desc"`. Defaults to `created_at:asc`. */
570
+ readonly order?: string;
571
+ /** Escape hatch: extra `search[...]` keys. Unknown keys are a 400. */
572
+ readonly search?: Record<string, QueryValue>;
573
+ /** Escape hatch: extra `exact_search[...]` keys. Unknown keys are a 400. */
574
+ readonly exactSearch?: Record<string, QueryValue>;
575
+ }
576
+ /** Arguments for {@link LibraryAnnotationsNamespace.create}. */
577
+ export interface CreateBookAnnotationInput {
578
+ readonly bookId: BookId;
579
+ readonly kind: BookAnnotationKind;
580
+ /** Omitted or empty becomes `{}`, which is a valid annotation with no anchor. */
581
+ readonly location?: BookAnnotationLocation;
582
+ readonly color?: string | null;
583
+ readonly note?: string | null;
584
+ readonly selectedText?: string | null;
585
+ }
586
+ /**
587
+ * Arguments for {@link LibraryAnnotationsNamespace.update}.
588
+ *
589
+ * `kind`, `book_id` and `selected_text` are NOT updatable: `update_params` on
590
+ * the controller lists `color` and `note` only, and `location` is spliced in by
591
+ * hand. Anything else is dropped in silence by `permit` - a 200 with an
592
+ * unchanged row.
593
+ */
594
+ export interface UpdateBookAnnotationInput {
595
+ readonly color?: string | null;
596
+ readonly note?: string | null;
597
+ /**
598
+ * Only sent when the key is present, because the controller tests
599
+ * `params.key?(:location)` rather than presence. Passing `{}` therefore
600
+ * CLEARS the anchor rather than leaving it alone.
601
+ */
602
+ readonly location?: BookAnnotationLocation;
603
+ }
604
+ /** A byte range of a book file, as {@link LibraryBooksNamespace.fileRange} returns it. */
605
+ export interface BookFileRange {
606
+ /** The bytes. */
607
+ readonly data: ArrayBuffer;
608
+ /** First byte offset, inclusive, as the server reported it. */
609
+ readonly start: number;
610
+ /** Last byte offset, INCLUSIVE. HTTP ranges are closed at both ends. */
611
+ readonly end: number;
612
+ /** Total size of the file, from the `Content-Range` header. */
613
+ readonly total: number;
614
+ /** Pinned to the book's format server-side, never sniffed from the bytes. */
615
+ readonly contentType: string;
616
+ }
617
+ /** The `library.books` namespace, reachable as `oms.library.books`. */
618
+ export declare class LibraryBooksNamespace extends Resource {
619
+ /**
620
+ * `GET /books` - browse the library.
621
+ *
622
+ * WORKS ANONYMOUSLY, and what it returns depends on who is asking:
623
+ * `Book.listable_by` is every `public` book plus, for a signed-in caller,
624
+ * their own books whatever their visibility. `unlisted` books are absent by
625
+ * construction - that is what unlisted means - so a reader's own unlisted
626
+ * book DOES appear here (it is theirs) while somebody else's never will.
627
+ *
628
+ * Pagination is FORCED: `CrudActions#index_modifiers_params` gives a request
629
+ * with no page modifier `1:500`, and `QueryModifier` clamps any larger size
630
+ * back to 500. That is a DoS guard - an unbounded listing holds a Puma
631
+ * thread and a database connection for as long as the serialisation takes.
632
+ * The SDK always sends a page modifier, so {@link Paginated.pageSize} always
633
+ * reports the size the rows were counted against.
634
+ *
635
+ * Order defaults to whatever Postgres returns, which is NOT stable across
636
+ * pages. Pass an explicit `order` for any paged walk; `"created_at:desc"` is
637
+ * what every library screen uses. An unknown column in `order` is IGNORED in
638
+ * silence (`QueryModifier` checks `column_names` and returns), so a typo
639
+ * costs you the ordering and no error.
640
+ *
641
+ * The response carries an `ETag` and honours `If-None-Match` (except with
642
+ * `random`), so a repeated identical listing is cheap for the server even
643
+ * though the SDK does not cache it for you.
644
+ *
645
+ * @throws {OmsApiError} 400 naming the offending key when a filter is not
646
+ * one of `title`, `author`, `format`, `user_id`, `id`, `created_at`,
647
+ * `updated_at`. Filters fail closed on purpose: silently dropping an
648
+ * unknown one used to answer with the UNFILTERED set.
649
+ */
650
+ list(params?: ListBooksParams, options?: RequestOptions): Promise<Paginated<Book>>;
651
+ /**
652
+ * The caller's own books, whatever their visibility. Sugar for
653
+ * {@link list} with `scope: "mine"`.
654
+ *
655
+ * Needs a credential to mean anything: `apply_scope` narrows to
656
+ * `user_id: Current.user&.id`, and for an anonymous caller that is
657
+ * `user_id: nil`, which matches no row. An anonymous call therefore returns
658
+ * an EMPTY list rather than a 401 - the endpoint is genuinely public and has
659
+ * nothing to complain about.
660
+ */
661
+ mine(params?: Omit<ListBooksParams, "scope">, options?: RequestOptions): Promise<Paginated<Book>>;
662
+ /**
663
+ * Other people's public books. Sugar for {@link list} with
664
+ * `scope: "explore"`, or with `ownerHandle` when one is given.
665
+ *
666
+ * The scope is DROPPED as soon as a handle is supplied, matching the web
667
+ * client: asking for "everybody but me, and also only this person" reads as
668
+ * a filter but behaves as a way to make your own library invisible when you
669
+ * pass your own handle.
670
+ */
671
+ explore(params?: Omit<ListBooksParams, "scope">, options?: RequestOptions): Promise<Paginated<Book>>;
672
+ /**
673
+ * `GET /books/:id` - one book.
674
+ *
675
+ * The route an `unlisted` share link resolves through, so it works
676
+ * anonymously and its scope (`Book.viewable_by`) is deliberately WIDER than
677
+ * the listing's: public, unlisted, plus your own.
678
+ *
679
+ * Renders the `:extended` view, which `BookBlueprint` leaves identical to the
680
+ * default one, so this returns exactly what a row of {@link list} carries.
681
+ *
682
+ * @throws {OmsApiError} 404 `"Resource not found"` - for an id that does not
683
+ * exist AND for a private book belonging to someone else, indistinguishably.
684
+ * The scope is applied before the id is compared, which is what stops the
685
+ * endpoint being an existence oracle for other people's private books.
686
+ */
687
+ get(id: BookId, options?: RequestOptions): Promise<Book>;
688
+ /**
689
+ * `POST /books` - uploads a PDF or an EPUB and adds it to the library.
690
+ *
691
+ * Multipart, synchronous, and slow: Rails buffers the whole body, writes the
692
+ * file to storage, and derives a WebP thumbnail when a cover came with it.
693
+ * The per-attempt deadline defaults to five minutes here instead of the
694
+ * client's usual one, and the call is NOT retried - a replay after a lost
695
+ * answer uploads the book twice.
696
+ *
697
+ * ## Two size ceilings, and the one you will actually hit
698
+ *
699
+ * Rails refuses anything over {@link BOOK_MAX_FILE_BYTES} (100 MiB) with
700
+ * `413 "File too big (max 100 MB)"`. In production that message is nearly
701
+ * unreachable, because Cloudflare sits in front and refuses a request body
702
+ * over roughly {@link BOOK_UPLOAD_EDGE_LIMIT_BYTES} with its OWN `413`
703
+ * before the origin sees a byte. That one arrives as an
704
+ * {@link OmsApiError} whose body is an HTML error page and which mentions
705
+ * neither books nor a limit, so a caller matching on the message will not
706
+ * recognise it. Match on `status === 413` instead, and say "the file is too
707
+ * large" whatever the body claims.
708
+ *
709
+ * There is no chunked route for books, so splitting the file is not an
710
+ * option; a 400 MB scan has to be shrunk before it can be uploaded. (This is
711
+ * the same ceiling `songs.import` documents, hit far more often here: a
712
+ * scanned book is routinely bigger than an album.)
713
+ *
714
+ * This method checks the size LOCALLY first when the size is knowable - a
715
+ * `Blob`, a `Uint8Array`, or a picked file whose descriptor reported one -
716
+ * and throws before spending several minutes uploading something that will
717
+ * be refused. A `ReadableStream` reports nothing and is sent as-is.
718
+ *
719
+ * ## What the server decides for you
720
+ *
721
+ * - **Format**, from the filename extension, falling back to the content
722
+ * type. A `.pdf` that is really an EPUB is stored as a PDF and will not
723
+ * open; the reader pins `Content-Type` to this value and sends
724
+ * `X-Content-Type-Options: nosniff`, so nothing downstream corrects it.
725
+ * - **Title**, when you send none: the filename without its extension.
726
+ * - **Visibility**: always `private`. `BookServices::Creator` never reads the
727
+ * field, so passing one changes nothing. Sharing a freshly uploaded book is
728
+ * always a second call to {@link update}.
729
+ * - **`file_size_bytes`**, from the stored node. Not from anything you send.
730
+ *
731
+ * @throws {TypeError} before any request when the file is over the edge
732
+ * ceiling and its size was knowable locally.
733
+ * @throws {OmsAuthError} 401 `"Session required"` when anonymous. Note it is
734
+ * checked in the action, so this is a 401 and not the 403 an OAuth token
735
+ * would get.
736
+ * @throws {OmsApiError} 413 for an oversized body (from Rails, or from the
737
+ * edge - see above); 400 `"No file provided"`, `"Unsupported file type,
738
+ * only PDF and EPUB are accepted"`, `"Library storage unavailable"`, or the
739
+ * model's validation messages.
740
+ */
741
+ create(input: CreateBookInput, options?: RequestOptions): Promise<Book>;
742
+ /**
743
+ * `PATCH /books/:id` - edits metadata, visibility, and optionally the cover.
744
+ *
745
+ * JSON normally; multipart as soon as {@link UpdateBookInput.cover} is
746
+ * present, because that is the only way to carry a file. Both encodings reach
747
+ * the same code path, and this method papers over the one place they would
748
+ * otherwise differ: clearing a column. Every multipart field is a string, so
749
+ * there is no `null` to send - the backend's `\b` sentinel is decoded for
750
+ * update params exactly as it is for filters, and this writes it for you when
751
+ * you pass `null`.
752
+ *
753
+ * This is where a book becomes shareable. `visibility` is the only field that
754
+ * changes who can reach it, and it cannot be set at upload time.
755
+ *
756
+ * Replacing the cover leaves the OLD cover nodes behind as orphans until the
757
+ * book itself is destroyed: `update_params` creates a new node and points the
758
+ * book at it, and only `compress_cover` destroys the previous compressed
759
+ * twin. Not a bug you can fix from here, but it is why re-cropping a cover
760
+ * repeatedly costs storage quota.
761
+ *
762
+ * @throws {OmsAuthError} 401 when the book is not yours - `CrudActions#update`
763
+ * checks `updatable_by?` and answers 401, not 403.
764
+ * @throws {OmsApiError} 404 when the id is unknown or the book is someone
765
+ * else's PRIVATE one (the lookup is scoped first); 400 with the model's
766
+ * validation messages, e.g. a `visibility` outside
767
+ * {@link BOOK_VISIBILITIES} or a title over
768
+ * {@link BOOK_TITLE_MAX_LENGTH}.
769
+ */
770
+ update(id: BookId, input: UpdateBookInput, options?: RequestOptions): Promise<Book>;
771
+ /**
772
+ * `DELETE /books/:id` - removes the book, its file, its covers, its
773
+ * annotations and its place on every shelf.
774
+ *
775
+ * Genuinely destructive and not recoverable: `file_fs_node` is
776
+ * `dependent: :destroy`, so the stored PDF or EPUB goes with the row, and so
777
+ * do every reader's highlights on it.
778
+ *
779
+ * @throws {OmsAuthError} 401 when the book is not yours.
780
+ * @throws {OmsApiError} 404 the second time, because the row is already gone.
781
+ * That is why this is never retried on a torn connection: a replay would
782
+ * report "not found" for a delete that worked perfectly well.
783
+ */
784
+ delete(id: BookId, options?: RequestOptions): Promise<void>;
785
+ /**
786
+ * `GET /books/libraries` - every reader who opted their library public and
787
+ * has at least one public book.
788
+ *
789
+ * A directory of PEOPLE, not of books, and the one payload here that is not a
790
+ * Blueprinter record - see {@link PublicLibrary}. Anonymous, unpaged, and
791
+ * cheap: the set is small by construction and the counts come from one
792
+ * grouped query.
793
+ *
794
+ * A reader appears only when BOTH halves are true: `library_public` on their
795
+ * user record (set through `account.update`, not from this namespace) AND at
796
+ * least one book at `public`. Making a single book public does not publish a
797
+ * library, and publishing a library with only unlisted books lists nobody.
798
+ */
799
+ libraries(options?: RequestOptions): Promise<PublicLibrary[]>;
800
+ /**
801
+ * `GET /books/:id/file` - the raw bytes, for an in-app reader.
802
+ *
803
+ * The one media route in this namespace that does NOT redirect: Rails serves
804
+ * the bytes itself, same-origin to the API, precisely so a reader
805
+ * (`react-pdf`, `epub.js`) can pull them over XHR without a cross-origin hop
806
+ * into object storage. It sends `Accept-Ranges: bytes`, pins `Content-Type`
807
+ * to the validated format, and sets `X-Content-Type-Options: nosniff` so a
808
+ * doctored `.pdf` full of HTML cannot be rendered as a document.
809
+ *
810
+ * Conditional GET is deliberately DISABLED on this route
811
+ * (`Cache-Control: no-cache, no-store, must-revalidate`): `Rack::ETag` would
812
+ * otherwise tag the body and the browser would revalidate into an empty `304`
813
+ * that an XHR reader cannot parse. So every call is a full transfer and
814
+ * caching is the client's job.
815
+ *
816
+ * **This reads the whole book into memory twice** - once in Rails
817
+ * (`send_data blob.download`) and once here. For a large PDF prefer
818
+ * {@link fileRange}, which is what a PDF viewer does on its own, or
819
+ * {@link fileUrl} plus a native downloader on a phone.
820
+ *
821
+ * Anonymous callers are limited to
822
+ * {@link ANONYMOUS_BOOK_FILE_RATE_LIMIT_PER_HOUR} reads an hour per IP across
823
+ * this route and {@link download} together; a signed-in caller is not
824
+ * limited by it. The route is also exempt from the general 600/min ceiling,
825
+ * so a Range-reading viewer opening one book cannot trip it.
826
+ *
827
+ * @throws {OmsApiError} 404 `"No file attached"` when the row exists but its
828
+ * node lost its blob, and `"Resource not found"` when the book is not
829
+ * visible to the caller; 429 for an anonymous caller over the hourly cap.
830
+ */
831
+ file(id: BookId, options?: RequestOptions): Promise<FileOutput>;
832
+ /**
833
+ * `GET /books/:id/file` with a `Range` header - one slice of the book.
834
+ *
835
+ * This is how a PDF viewer opens a 60 MB scan in under a second: it reads the
836
+ * trailer, then the pages it needs, and never downloads the rest. The server
837
+ * answers `206` with `Content-Range` and serves the slice through
838
+ * `blob.service.download_chunk`, so the origin does not read the whole file
839
+ * either.
840
+ *
841
+ * `end` is INCLUSIVE, as HTTP ranges are: `{ start: 0, end: 1023 }` is the
842
+ * first 1024 bytes. Omit `end` for "from here to the end of the file". A
843
+ * suffix range (the LAST n bytes) is not offered here because the server's
844
+ * `Content-Range` is the only way back to absolute offsets and this method
845
+ * would have to guess; send the header yourself through `http.raw` if you
846
+ * need one.
847
+ *
848
+ * ONE range per request. `BooksController#send_file_range` answers `416` to a
849
+ * multi-range header rather than building a multipart/byteranges body, so do
850
+ * not batch.
851
+ *
852
+ * @throws {OmsApiError} 416 when the range starts past the end of the file,
853
+ * or when more than one range was asked for. The response carries
854
+ * a `Content-Range` naming the real size, with a star where the range
855
+ * would be. That is how a caller learns the size after guessing wrong.
856
+ * @throws {OmsError} when the server answered `200` instead of `206` - a
857
+ * proxy that stripped the `Range` header, which would otherwise be read as
858
+ * a slice and quietly corrupt the reader's buffer.
859
+ */
860
+ fileRange(id: BookId, range: {
861
+ start: number;
862
+ end?: number;
863
+ }, options?: RequestOptions): Promise<BookFileRange>;
864
+ /**
865
+ * Absolute URL of the book's bytes, for a reader that fetches them itself.
866
+ *
867
+ * Synchronous, and it carries NO CREDENTIAL - it cannot, because a token
868
+ * provider may be async and this is called while a component renders. What
869
+ * that means in practice depends on the book:
870
+ *
871
+ * - a `public` or `unlisted` book opens with no credential at all;
872
+ * - a `private` one needs one, and the caller has to attach it. For pdf.js
873
+ * that is `httpHeaders: { Authorization: 'Bearer ...' }` on a token client,
874
+ * or `withCredentials: true` on a cookie client. Both, on a client that has
875
+ * both, would be a request the API refuses to disambiguate.
876
+ *
877
+ * Prefer NOT to append `?token=...`. The API does accept a query token
878
+ * (`Session.candidate_tokens` reads `params[:token]`) and the web frontend
879
+ * uses it for `cover` and `download`, but it puts a live session credential
880
+ * into access logs and into anything that copies the URL - a pasted link
881
+ * hands over the account, and the token never expires on its own.
882
+ *
883
+ * The rate-limit half of that warning used to be stated here and was wrong:
884
+ * a query token is NOT counted as anonymous. `Rack::Attack.authed_session`
885
+ * resolves through `Session.find_by_request_readonly`, which calls
886
+ * `resolve_from_request` with `allow_query_param: true` by default, so a
887
+ * valid `?token=` resolves to a live session and the request is keyed
888
+ * `sess:<id>` in the authenticated 600/min bucket. Only
889
+ * `find_by_browser_request` drops the query param, and it is used solely by
890
+ * the HTML consent and device pages. The leak is the reason to avoid it; the
891
+ * budget is not.
892
+ */
893
+ fileUrl(id: BookId): string;
894
+ /**
895
+ * `GET /books/:id/cover` - `302` to the cover image in object storage.
896
+ *
897
+ * Prefer {@link coverUrl} in markup. This method exists for when the bytes
898
+ * themselves are wanted, and it sends the caller's credential because it has
899
+ * to: the action resolves the book through `Book.viewable_by(Current.user)`,
900
+ * so your own private book's cover is a `404` without one.
901
+ *
902
+ * ## It does not work from a browser, and that is not fixable here
903
+ *
904
+ * The `302` goes to `minio.omelhorsite.pt`. Per the Fetch standard a CORS
905
+ * request redirected cross-origin gets an opaque origin on the next hop, MinIO
906
+ * answers a null origin with `Access-Control-Allow-Origin: *`, and a wildcard
907
+ * is illegal for a credentialed request - so the browser rejects the response
908
+ * before any JavaScript sees it. `account.picture` solves this by sending no
909
+ * credential; that is not available here, because a private cover needs one.
910
+ *
911
+ * In a browser, put {@link coverUrl} in an `<img>` and let the platform
912
+ * follow the redirect with no CORS check at all. In Bun, in a Worker and on
913
+ * React Native, where no CORS is enforced, this method is fine.
914
+ *
915
+ * @throws {OmsApiError} 404 `"No cover"` when the book has none, and
916
+ * `"Resource not found"` when it is not visible to the caller.
917
+ */
918
+ cover(id: BookId, options?: RequestOptions): Promise<FileOutput>;
919
+ /**
920
+ * Absolute URL of the cover, for an `<img>` or a `background-image`.
921
+ *
922
+ * Synchronous, uncredentialed, and safe to put in markup for a `public` or
923
+ * `unlisted` book. A PRIVATE book's cover answers `404` through this URL
924
+ * unless the platform attaches the session cookie for you - which an `<img>`
925
+ * on the same site does and an element with `crossorigin="anonymous"` does
926
+ * not. Give the element an `onError` that falls back to a generated cover.
927
+ *
928
+ * The redirect target is a presigned URL, so do not cache this URL's
929
+ * RESOLVED destination anywhere; cache this URL instead.
930
+ */
931
+ coverUrl(id: BookId): string;
932
+ /**
933
+ * `GET /books/:id/download` - `302` to the book file as an attachment.
934
+ *
935
+ * The difference from {@link file} is only the disposition: this one is
936
+ * `attachment` with a slugged filename (`war-and-peace.pdf`), and it goes
937
+ * through object storage rather than through Rails. Same CORS caveat as
938
+ * {@link cover} - see it - and the same anonymous ceiling of
939
+ * {@link ANONYMOUS_BOOK_FILE_RATE_LIMIT_PER_HOUR} per hour per IP.
940
+ *
941
+ * On a phone, do not pull a whole book through JavaScript for the sake of
942
+ * saving it: hand {@link downloadUrl} to a native downloader.
943
+ */
944
+ download(id: BookId, options?: RequestOptions): Promise<FileOutput>;
945
+ /**
946
+ * Absolute URL that saves the book as a file. Synchronous and
947
+ * uncredentialed, with the same visibility caveat as {@link coverUrl}.
948
+ *
949
+ * Suitable for an `<a href>` or for a native downloader. The filename comes
950
+ * from the server's `Content-Disposition`, so there is no need for a
951
+ * `download` attribute - which a cross-origin link ignores anyway.
952
+ */
953
+ downloadUrl(id: BookId): string;
954
+ /**
955
+ * `POST /books/:id/progress` - upserts "where I left off" for this reader.
956
+ *
957
+ * An UPSERT, not a create: the controller does
958
+ * `first_or_initialize` on `(book, user, kind: "progress")`, so there is
959
+ * exactly one progress marker per reader per book however many times this is
960
+ * called, and no id is needed. Call it freely as the reader turns pages.
961
+ *
962
+ * Private to the reader even on a public book, and it works on somebody
963
+ * else's public book - which is the point: reading a shared book should
964
+ * remember where you were.
965
+ *
966
+ * `location` is opaque `jsonb`. A blank or omitted one is stored as `{}`,
967
+ * which reads back as "no position recorded" rather than as page zero.
968
+ *
969
+ * Returns the annotation, so the row that comes back is the same shape
970
+ * {@link LibraryAnnotationsNamespace} deals in and can be dropped straight
971
+ * into a client-side cache of markers.
972
+ *
973
+ * @throws {OmsAuthError} 401 when anonymous - unlike the read routes, this
974
+ * one is not in `allow_unauthenticated_access`.
975
+ * @throws {OmsApiError} 404 when the book is not visible to the caller.
976
+ */
977
+ saveProgress(id: BookId, location: BookAnnotationLocation, options?: RequestOptions): Promise<BookAnnotation>;
978
+ /**
979
+ * Builds the query for {@link list}.
980
+ *
981
+ * `title` and `author` go in `search` (partial, accent-insensitive); the
982
+ * exact-match columns go in `exact_search`; `scope`, `owner_handle` and
983
+ * `tags` go in `extra_options`, which is a THIRD bucket with its own
984
+ * allowlist - `BooksController` declares only those three, so any other key
985
+ * there is a 400.
986
+ */
987
+ private bookQuery;
988
+ /** JSON body for an update. `null` stays `null`; the transport does not touch a body. */
989
+ private updateBody;
990
+ /**
991
+ * Multipart fields for an update, with the one encoding difference fixed.
992
+ *
993
+ * `null` becomes the `\b` sentinel, which `CrudActions` decodes back to `nil`
994
+ * for update params exactly as it does for filters - a form field has no
995
+ * other way to say "clear this column". An empty `tags` list is sent as a
996
+ * single empty string, because appending an empty array appends nothing and
997
+ * an absent key means "leave the tags alone", which is the opposite of what
998
+ * clearing them means. `normalize_tags` drops the blank entry, leaving `[]`.
999
+ */
1000
+ private multipartUpdateFields;
1001
+ }
1002
+ /** The `library.shelves` namespace, reachable as `oms.library.shelves`. */
1003
+ export declare class LibraryShelvesNamespace extends Resource {
1004
+ /**
1005
+ * `GET /book_shelves` - browse shelves.
1006
+ *
1007
+ * Same two-axis visibility rule as books: `BookShelf.listable_by` is every
1008
+ * `public` shelf plus, for a signed-in caller, their own whatever their
1009
+ * visibility. An `unlisted` shelf opens by its link and appears in no
1010
+ * listing.
1011
+ *
1012
+ * Ordered by `(position, created_at)` unless you override it: the controller
1013
+ * chains `.ordered` onto the listing scope, so the owner's own arrangement is
1014
+ * the default and a paged walk is stable without asking.
1015
+ *
1016
+ * **The rows here carry no `books`.** `index` renders the default view and
1017
+ * only the `:extended` one inlines the association. Every other method in
1018
+ * this namespace returns the extended shape; this one does not. Do not build
1019
+ * a shelf screen by listing and reading `books` - it is `undefined`.
1020
+ *
1021
+ * Note also that `book_count` costs one query per shelf
1022
+ * (`shelf.visible_books(user).count`, per row, un-eager-loadable because it
1023
+ * depends on who is asking). A 100-shelf page is 100 extra counts. Page it.
1024
+ *
1025
+ * @throws {OmsApiError} 400 naming the offending key when a filter is not
1026
+ * one of `user_id`, `visibility`, `id`, `created_at`, `updated_at`. There
1027
+ * are no `extra_options` on this controller at all, so ANY key in that
1028
+ * bucket is a 400.
1029
+ */
1030
+ list(params?: ListBookShelvesParams, options?: RequestOptions): Promise<Paginated<BookShelf>>;
1031
+ /**
1032
+ * `GET /book_shelves/:id` - one shelf, WITH its books.
1033
+ *
1034
+ * The `:extended` view, so {@link BookShelf.books} is populated here and
1035
+ * nowhere in {@link list}. The books are filtered to what the ASKING user may
1036
+ * open, in shelf order: a shelf that holds a book its owner later made
1037
+ * private again shows that book to the owner and silently omits it for
1038
+ * everyone else, which is also why `book_count` and `books.length` can
1039
+ * disagree between two viewers of the same shelf.
1040
+ *
1041
+ * Works anonymously, through the wider `viewable_by` scope, which is how an
1042
+ * `unlisted` shelf link resolves.
1043
+ *
1044
+ * @throws {OmsApiError} 404 for an unknown id and for someone else's private
1045
+ * shelf, indistinguishably.
1046
+ */
1047
+ get(id: BookShelfId, options?: RequestOptions): Promise<BookShelf>;
1048
+ /**
1049
+ * `POST /book_shelves` - creates an empty shelf owned by the caller.
1050
+ *
1051
+ * The owner is taken from the session and cannot be sent: `create_params`
1052
+ * merges `Current.user` in after permitting the three writable fields.
1053
+ * Visibility defaults to `private`.
1054
+ *
1055
+ * Not retried by default, like every create: a replay after a lost answer
1056
+ * makes a second identical shelf, and shelves are capped per user.
1057
+ *
1058
+ * @throws {OmsAuthError} 401 when anonymous - `creatable_by?` is simply
1059
+ * `user.present?`.
1060
+ * @throws {OmsApiError} 400 `"You have reached the maximum of 100 shelves"`
1061
+ * at {@link BOOK_SHELVES_MAX_PER_USER}, or the model's validation messages
1062
+ * for a missing or over-long name.
1063
+ */
1064
+ create(input: CreateBookShelfInput, options?: RequestOptions): Promise<BookShelf>;
1065
+ /**
1066
+ * `PATCH /book_shelves/:id` - renames, re-describes, re-shares, or moves the
1067
+ * shelf among the owner's other shelves.
1068
+ *
1069
+ * {@link UpdateBookShelfInput.position} orders SHELVES, not the books on one.
1070
+ * Nothing renumbers the others, so two shelves can share a position and the
1071
+ * `(position, created_at)` sort then decides between them by age.
1072
+ *
1073
+ * @throws {OmsAuthError} 401 when the shelf is not yours.
1074
+ * @throws {OmsApiError} 404 for an unknown id or someone else's private
1075
+ * shelf; 400 with the model's validation messages.
1076
+ */
1077
+ update(id: BookShelfId, input: UpdateBookShelfInput, options?: RequestOptions): Promise<BookShelf>;
1078
+ /**
1079
+ * `DELETE /book_shelves/:id` - removes the shelf.
1080
+ *
1081
+ * The BOOKS SURVIVE. Only the `book_shelf_items` join rows are destroyed, so
1082
+ * this un-files the books rather than deleting them - the opposite of
1083
+ * {@link LibraryBooksNamespace.delete}, and worth saying in the confirmation
1084
+ * dialog.
1085
+ *
1086
+ * @throws {OmsAuthError} 401 when the shelf is not yours.
1087
+ * @throws {OmsApiError} 404 the second time round.
1088
+ */
1089
+ delete(id: BookShelfId, options?: RequestOptions): Promise<void>;
1090
+ /**
1091
+ * `POST /book_shelves/:id/add_book` - puts one of YOUR books on the shelf.
1092
+ *
1093
+ * IDEMPOTENT: `find_or_initialize_by` means adding a book that is already
1094
+ * there is a no-op with a `200`, not a duplicate and not a 422. Position is
1095
+ * assigned automatically at the end of the shelf.
1096
+ *
1097
+ * The book must be the shelf owner's own. `Current.user.books.find_by` is
1098
+ * what resolves it, so somebody else's book - public or not - is a `404
1099
+ * "Book not found"` rather than a permission error, and a shelf can never
1100
+ * hold a book whose owner might later make it private and leak it through
1101
+ * the shelf's visibility.
1102
+ *
1103
+ * Answers the whole shelf in its `:extended` view, so the response already
1104
+ * contains the new arrangement and there is no need to re-fetch.
1105
+ *
1106
+ * @throws {OmsApiError} 404 `"Shelf not found"` for an unknown shelf,
1107
+ * `"Book not found"` for a book that is not yours; 403 `"Not your shelf"`
1108
+ * for someone else's shelf - note this action answers **403** where
1109
+ * {@link update} answers 401 for the same offence.
1110
+ * @throws {OmsApiError} 422 with the item's validation messages, which in
1111
+ * practice only fires if the ownership check is ever relaxed.
1112
+ */
1113
+ addBook(id: BookShelfId, bookId: BookId, options?: RequestOptions): Promise<BookShelf>;
1114
+ /**
1115
+ * `DELETE /book_shelves/:id/remove_book` - takes a book off the shelf.
1116
+ *
1117
+ * The book itself is untouched; only the join row goes.
1118
+ *
1119
+ * IDEMPOTENT to the point of being silent: the action is
1120
+ * `find_by(...)&.destroy`, so removing a book that was never on the shelf
1121
+ * answers `200` with the unchanged shelf. There is no way to tell the two
1122
+ * apart from the response - compare `book_count` yourself if you need to.
1123
+ *
1124
+ * The book id travels as a QUERY PARAMETER rather than in a body. Rails reads
1125
+ * `params[:book_id]` from either, and a `DELETE` with a body is the shape
1126
+ * that intermediaries are least reliable about forwarding; the transport
1127
+ * also has no body slot on `delete`. Nothing here is a filter bucket, so the
1128
+ * unknown-key guard does not apply.
1129
+ *
1130
+ * @throws {OmsApiError} 404 `"Shelf not found"`; 403 `"Not your shelf"`.
1131
+ */
1132
+ removeBook(id: BookShelfId, bookId: BookId, options?: RequestOptions): Promise<BookShelf>;
1133
+ /**
1134
+ * `POST /book_shelves/:id/reorder` - sets the order of the books on a shelf.
1135
+ *
1136
+ * SEND THE COMPLETE LIST, in the order you want, every time. The action
1137
+ * writes `position = index + 1` for each id it was given and touches nothing
1138
+ * else, so a partial list leaves the omitted books on their OLD positions -
1139
+ * which now collide with the new ones, and the shelf comes back interleaved
1140
+ * in an order nobody chose. There is no error: it is a `200` and a shelf that
1141
+ * looks shuffled.
1142
+ *
1143
+ * Ids that are not on this shelf are ignored in silence (the `update_all`
1144
+ * matches no row), and so are ids of books that do not exist. So this cannot
1145
+ * be used to add a book - use {@link addBook} first, then reorder.
1146
+ *
1147
+ * The whole thing runs in one transaction, so a shelf is never left half
1148
+ * renumbered.
1149
+ *
1150
+ * @throws {TypeError} before any request for an empty list, which the server
1151
+ * would answer `400 "No order given"`.
1152
+ * @throws {OmsApiError} 404 `"Shelf not found"`; 403 `"Not your shelf"`.
1153
+ */
1154
+ reorder(id: BookShelfId, bookIds: readonly BookId[], options?: RequestOptions): Promise<BookShelf>;
1155
+ /** Builds the query for {@link list}. Both filterable columns are exact-match. */
1156
+ private shelfQuery;
1157
+ }
1158
+ /** The `library.annotations` namespace, reachable as `oms.library.annotations`. */
1159
+ export declare class LibraryAnnotationsNamespace extends Resource {
1160
+ /**
1161
+ * `GET /book_annotations` - the caller's own highlights, notes, bookmarks and
1162
+ * progress markers.
1163
+ *
1164
+ * There is no owner filter and there does not need to be one:
1165
+ * `BookAnnotation.viewable_by` is `where(user_id: user.id)`, so a caller can
1166
+ * only ever see its own rows - even on a book a thousand people have public
1167
+ * access to.
1168
+ *
1169
+ * Unlike every read in the other two namespaces, this controller declares no
1170
+ * `allow_unauthenticated_access`, so an anonymous call is a `401`, not an
1171
+ * empty list.
1172
+ *
1173
+ * Ordered by `created_at` ascending by default (the controller adds the
1174
+ * `order(:created_at)` itself), which is reading order for a set of
1175
+ * highlights. Override it with `order` for a "recently annotated" view.
1176
+ *
1177
+ * Filtering by `bookId` is the normal call, and it is what opens a reader:
1178
+ * one request per book, not one per highlight.
1179
+ *
1180
+ * @throws {OmsApiError} 400 naming the offending key when a filter is not
1181
+ * one of `book_id`, `kind`, `id`, `created_at`, `updated_at`.
1182
+ */
1183
+ list(params?: ListBookAnnotationsParams, options?: RequestOptions): Promise<Paginated<BookAnnotation>>;
1184
+ /**
1185
+ * Every annotation on one book, in reading order, following pages until the
1186
+ * book is exhausted.
1187
+ *
1188
+ * A convenience over {@link list} because this is what a reader does on open
1189
+ * and because a heavily annotated book genuinely passes 500 rows - the point
1190
+ * at which one page stops being enough and a naive caller silently loses the
1191
+ * rest.
1192
+ *
1193
+ * `kinds` narrows to, say, highlights only. It becomes an `IN (...)`.
1194
+ */
1195
+ forBook(bookId: BookId, params?: {
1196
+ kinds?: readonly BookAnnotationKind[];
1197
+ pageSize?: number;
1198
+ }, options?: RequestOptions): Promise<BookAnnotation[]>;
1199
+ /**
1200
+ * The reader's "where I left off" marker for one book, or `null` when they
1201
+ * have never opened it.
1202
+ *
1203
+ * There is at most one, because
1204
+ * {@link LibraryBooksNamespace.saveProgress} upserts on
1205
+ * `(book, user, kind: "progress")`. This reads it back; that writes it.
1206
+ */
1207
+ progressFor(bookId: BookId, options?: RequestOptions): Promise<BookAnnotation | null>;
1208
+ /**
1209
+ * "What am I in the middle of?" - the reader's most recently touched progress
1210
+ * markers, newest first.
1211
+ *
1212
+ * One request for the whole continue-reading shelf, which is the point: the
1213
+ * alternative is one request per cover. The rows carry `book_id` and nothing
1214
+ * about the book, so pair it with
1215
+ * {@link LibraryBooksNamespace.list} filtered by `ids` to get the titles in a
1216
+ * second request rather than in N.
1217
+ *
1218
+ * @param limit rows to return. Clamped to 500 by the server like any page.
1219
+ */
1220
+ continueReading(limit?: number, options?: RequestOptions): Promise<BookAnnotation[]>;
1221
+ /**
1222
+ * `POST /book_annotations` - records a highlight, a note or a bookmark.
1223
+ *
1224
+ * The author is taken from the session (`set_user_as_current_user`), so
1225
+ * `user_id` is not sendable. `creatable_by?` additionally requires the book to
1226
+ * be in `Book.viewable_by(user)`, which is what lets a reader annotate
1227
+ * somebody else's public book while keeping the annotation entirely private
1228
+ * to them.
1229
+ *
1230
+ * Do NOT use this for reading progress. It would create a SECOND `progress`
1231
+ * row - nothing here de-duplicates by kind - and the reader would then have
1232
+ * two conflicting "where I left off" markers with no way to tell which is
1233
+ * live. {@link LibraryBooksNamespace.saveProgress} upserts; this does not.
1234
+ *
1235
+ * Not retried by default: a replay after a lost answer duplicates the
1236
+ * highlight.
1237
+ *
1238
+ * @throws {OmsAuthError} 401 twice over, with different bodies: `"Session
1239
+ * required to access this resource."` from the auth filter when anonymous,
1240
+ * and `"You are not authorized to create this resource"` when the book is
1241
+ * not in `Book.viewable_by` - `CrudActions#create` turns a failed
1242
+ * `creatable_by?` into a 401 rather than the 404 you might expect.
1243
+ * @throws {OmsApiError} 400 for a `kind` outside
1244
+ * {@link BOOK_ANNOTATION_KINDS}, or a note or selection over its length
1245
+ * cap.
1246
+ */
1247
+ create(input: CreateBookAnnotationInput, options?: RequestOptions): Promise<BookAnnotation>;
1248
+ /**
1249
+ * `PATCH /book_annotations/:id` - edits the note or the colour of an existing
1250
+ * annotation, and optionally moves it.
1251
+ *
1252
+ * Only `color`, `note` and `location` are writable. `kind`, `book_id` and
1253
+ * `selected_text` are NOT in `update_params` and are dropped in silence by
1254
+ * `permit` - a `200` with an unchanged row, which is the failure mode worth
1255
+ * knowing about here. To change a highlight into a note, delete and recreate.
1256
+ *
1257
+ * `location` is only sent when you pass one, because the controller tests
1258
+ * `params.key?(:location)`: passing `{}` therefore CLEARS the anchor rather
1259
+ * than leaving it alone.
1260
+ *
1261
+ * @throws {OmsAuthError} 401 when the annotation is not yours.
1262
+ * @throws {OmsApiError} 404 for an unknown id - and for anyone else's
1263
+ * annotation, since `viewable_by` never returns one.
1264
+ */
1265
+ update(id: BookAnnotationId, input: UpdateBookAnnotationInput, options?: RequestOptions): Promise<BookAnnotation>;
1266
+ /**
1267
+ * `DELETE /book_annotations/:id` - removes one annotation.
1268
+ *
1269
+ * Deleting a `progress` row is how a reader is put back at the start of a
1270
+ * book; there is no other reset.
1271
+ *
1272
+ * @throws {OmsAuthError} 401 when the annotation is not yours.
1273
+ * @throws {OmsApiError} 404 the second time round.
1274
+ */
1275
+ delete(id: BookAnnotationId, options?: RequestOptions): Promise<void>;
1276
+ /** Builds the query for {@link list}. Both filterable columns are exact-match. */
1277
+ private annotationQuery;
1278
+ }
1279
+ /** Options for {@link BookChatNamespace.stream} and {@link BookChatNamespace.ask}. */
1280
+ export interface BookChatStreamOptions extends RequestOptions {
1281
+ /**
1282
+ * How long to wait for the NEXT piece of the answer before giving up, in
1283
+ * milliseconds. Defaults to the transport's 45 seconds; `0` disables it.
1284
+ *
1285
+ * A silence limit, not a total: an answer that keeps producing runs as long
1286
+ * as it likes. It exists because a stalled sidecar once answered `200` and
1287
+ * then said nothing for two minutes at a time, and `await reader.read()` has
1288
+ * no deadline of its own, so the chat panel span for as long as the tab
1289
+ * stayed open. `timeoutMs` cannot cover this: it is disposed the moment the
1290
+ * headers arrive, or no stream could outlive it.
1291
+ *
1292
+ * It has to sit well clear of a cold model's first token - the 8B model has
1293
+ * to be paged in before it can answer - which is why 45 seconds and not five.
1294
+ *
1295
+ * IGNORED on the buffered path (React Native), where there is one read and
1296
+ * the caller's `signal` is what bounds it.
1297
+ */
1298
+ readonly silenceTimeoutMs?: number;
1299
+ }
1300
+ /**
1301
+ * The `library.chat` namespace: the reader's study assistant.
1302
+ *
1303
+ * `POST /books/:id/chat` is the only STREAMING endpoint in the whole API. It is
1304
+ * its own controller rather than an action on `BooksController` for a concrete
1305
+ * reason: `ActionController::Live` wraps every action of the controller it is
1306
+ * included in, and `Live::Response` drops `Content-Length` on first write -
1307
+ * which is exactly the header `GET /books/:id/file` must keep, because pdf.js
1308
+ * reads it to decide whether the server supports range requests.
1309
+ *
1310
+ * ## What it costs, and why it refuses rather than queues
1311
+ *
1312
+ * One request parks a Puma thread for the whole generation, tens of seconds on
1313
+ * the local model. `Ai::StreamSlots` caps how many generations run at once and
1314
+ * FAILS CLOSED when they are all taken - a caller parked waiting for a slot
1315
+ * would be holding the very thread the slots exist to protect. That refusal is
1316
+ * a `503`; see {@link isBookChatBusy}. On top of it, one client may only START
1317
+ * {@link BOOK_CHAT_RATE_LIMIT_PER_MINUTE} generations a minute, so a loop
1318
+ * cannot take whatever slot frees up next.
1319
+ *
1320
+ * ## The client owns the conversation
1321
+ *
1322
+ * There is no server-side session and no history storage. Every request carries
1323
+ * the whole conversation and the passage the question is about; what you do not
1324
+ * send did not happen. The server owns the framing and the caps, and it drops
1325
+ * any role other than `user` and `assistant` so a client cannot inject a
1326
+ * `system` turn into our own prompt.
1327
+ */
1328
+ export declare class BookChatNamespace extends Resource {
1329
+ /**
1330
+ * `POST /books/:id/chat` - asks the assistant and yields the answer as it
1331
+ * arrives.
1332
+ *
1333
+ * Yields TEXT DELTAS, already unwrapped from their SSE frames: concatenating
1334
+ * everything this yields is the complete answer. Framing is handled here
1335
+ * because a chunk is not a frame - a `data:` line can arrive split across two
1336
+ * network chunks - and every client that tried to parse the raw stream itself
1337
+ * got that wrong at least once.
1338
+ *
1339
+ * ## The two runtimes do not deliver the same way, and the difference is real
1340
+ *
1341
+ * - **browser and Bun**: one yield per delta, as the model produces it. A
1342
+ * typing UI works, and abandoning the loop (a `break`, or an early
1343
+ * `return`) cancels the reader, closes the connection, and the server sees
1344
+ * `ClientDisconnected` and RELEASES ITS GENERATION SLOT. Stopping early
1345
+ * genuinely stops the work.
1346
+ * - **React Native**: ONE yield, containing the entire answer, after the
1347
+ * server has finished generating. RN's `fetch` is `XMLHttpRequest` under a
1348
+ * shim, the whole body is accumulated natively and handed over at the end,
1349
+ * and `response.body` does not exist. This is not a gap to polyfill.
1350
+ * Consequences to design around: a typing indicator will sit still and then
1351
+ * snap to the finished answer, so ask {@link bookChatIsIncremental} and
1352
+ * show a spinner instead; and `break`ing out of the loop cancels NOTHING,
1353
+ * because the request already completed - only an `AbortSignal` can stop
1354
+ * the work, and only by tearing down the whole request.
1355
+ *
1356
+ * The final text is identical on both. Nothing is lost, nothing is
1357
+ * reordered, and no chunk boundary is invented to make one look like the
1358
+ * other.
1359
+ *
1360
+ * ## A `200` does not mean the answer worked
1361
+ *
1362
+ * The status and the SSE headers are written before the model has produced a
1363
+ * token, so every later failure is in-band. Two of them, both raised as
1364
+ * {@link BookChatError} with the text already yielded attached:
1365
+ *
1366
+ * - an `error` frame (`reason: "assistant_unavailable"`) - the inference
1367
+ * client raised, and the upstream message is deliberately withheld because
1368
+ * it can carry the sidecar's internals;
1369
+ * - the stream ending without a `done` frame (`reason: "truncated"`) - the
1370
+ * proxy dropped a stream that went quiet, or the connection was cut
1371
+ * mid-answer. Returning quietly here would report a half-answer as a
1372
+ * complete one, which is the whole reason the `done` frame exists.
1373
+ *
1374
+ * @example
1375
+ * ```ts
1376
+ * let answer = "";
1377
+ * for await (const delta of oms.library.chat.stream(bookId, { messages, context })) {
1378
+ * answer += delta;
1379
+ * render(answer); // one paint per delta, or one paint on RN
1380
+ * }
1381
+ * ```
1382
+ *
1383
+ * @throws {OmsAuthError} 401 `"Session required"` when anonymous.
1384
+ * @throws {OmsApiError} 404 `"Book not found"` when the book is not viewable;
1385
+ * 413 `"Request too big"` over {@link BOOK_CHAT_MAX_BODY_BYTES}; 429 over
1386
+ * {@link BOOK_CHAT_RATE_LIMIT_PER_MINUTE}; 503 when every generation slot
1387
+ * is taken ({@link isBookChatBusy}).
1388
+ * @throws {BookChatError} for either post-`200` failure. Check `reason` and
1389
+ * `partial`.
1390
+ * @throws {OmsTimeoutError} when the stream goes quiet for
1391
+ * {@link BookChatStreamOptions.silenceTimeoutMs}. Streaming path only.
1392
+ */
1393
+ stream(bookId: BookId, input: BookChatInput, options?: BookChatStreamOptions): AsyncGenerator<string, void, undefined>;
1394
+ /**
1395
+ * The same call as {@link stream}, waited out and handed back as one string.
1396
+ *
1397
+ * For the CLI, the MCP server, and anywhere a partial answer has nowhere to
1398
+ * go. It is exactly `stream` drained into a buffer, so it raises the same
1399
+ * errors for the same reasons - including {@link BookChatError} on a
1400
+ * truncated stream, which is the case a naive `await response.text()` would
1401
+ * hand back as a successful half-answer.
1402
+ *
1403
+ * It does NOT make the request cheaper or faster anywhere. On a browser and
1404
+ * on Bun the answer still streams in and is merely accumulated here; on React
1405
+ * Native it was going to arrive in one piece regardless. Choose it because
1406
+ * the CALLER has no use for pieces, never as a way to avoid streaming.
1407
+ */
1408
+ ask(bookId: BookId, input: BookChatInput, options?: BookChatStreamOptions): Promise<string>;
1409
+ }
1410
+ /**
1411
+ * The `library` namespace, reachable as `oms.library`.
1412
+ *
1413
+ * A thin holder for the four sub-namespaces. Each of them is exported on its
1414
+ * own too, so a host that would rather write `oms.books` can construct
1415
+ * {@link LibraryBooksNamespace} directly with the same `ApiClient`.
1416
+ */
1417
+ export declare class LibraryNamespace extends Resource {
1418
+ /** Books: upload, browse, share, read the bytes. */
1419
+ readonly books: LibraryBooksNamespace;
1420
+ /** Shelves: named, ordered, shareable groups of your own books. */
1421
+ readonly shelves: LibraryShelvesNamespace;
1422
+ /** Highlights, notes, bookmarks and reading progress. Always private. */
1423
+ readonly annotations: LibraryAnnotationsNamespace;
1424
+ /** The streaming study assistant. */
1425
+ readonly chat: BookChatNamespace;
1426
+ /**
1427
+ * `GET /books/libraries` - the public-library directory.
1428
+ *
1429
+ * Delegates to {@link LibraryBooksNamespace.libraries}. It lives on the books
1430
+ * route because that is where Rails mounted it, and it is surfaced here
1431
+ * because it is a directory of READERS, which is what a caller looking for it
1432
+ * expects `oms.library` to have.
1433
+ */
1434
+ publicLibraries(options?: RequestOptions): Promise<PublicLibrary[]>;
1435
+ }