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