@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.
- package/dist/index.js +4939 -552
- package/dist/types/client.d.ts +60 -3
- package/dist/types/http.d.ts +444 -19
- package/dist/types/index.d.ts +4 -1
- package/dist/types/resources/account.d.ts +66 -3
- package/dist/types/resources/admin.d.ts +1837 -0
- package/dist/types/resources/auth/index.d.ts +39 -0
- package/dist/types/resources/auth/passkeys.d.ts +652 -0
- package/dist/types/resources/auth/sessions.d.ts +847 -0
- package/dist/types/resources/chests.d.ts +54 -3
- package/dist/types/resources/content.d.ts +2970 -0
- package/dist/types/resources/dynamicQrs.d.ts +39 -3
- package/dist/types/resources/forms.d.ts +176 -35
- package/dist/types/resources/index.d.ts +19 -8
- package/dist/types/resources/ipLookup.d.ts +20 -4
- package/dist/types/resources/jobs.d.ts +62 -21
- package/dist/types/resources/library.d.ts +1435 -0
- package/dist/types/resources/linkTrees.d.ts +142 -30
- package/dist/types/resources/media.d.ts +351 -0
- package/dist/types/resources/movies.d.ts +1186 -0
- package/dist/types/resources/music/artists.d.ts +1066 -0
- package/dist/types/resources/music/imports.d.ts +940 -0
- package/dist/types/resources/music/index.d.ts +61 -0
- package/dist/types/resources/music/playlists.d.ts +1026 -0
- package/dist/types/resources/music/social.d.ts +1132 -0
- package/dist/types/resources/music/songs.d.ts +1183 -0
- package/dist/types/resources/notepads.d.ts +4 -1
- package/dist/types/resources/quotas.d.ts +7 -1
- package/dist/types/resources/realtime.d.ts +855 -0
- package/dist/types/resources/shortLinks.d.ts +45 -4
- package/dist/types/resources/social.d.ts +1330 -0
- package/dist/types/resources/storage/upload.d.ts +158 -11
- package/dist/types/resources/storage.d.ts +88 -22
- package/dist/types/resources/tickets.d.ts +82 -3
- package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
- package/dist/types/resources/tools/captions.d.ts +448 -21
- package/dist/types/resources/tools/downloader.d.ts +21 -0
- package/dist/types/resources/tools/index.d.ts +57 -15
- package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
- package/dist/types/resources/tools/transcription.d.ts +35 -13
- package/dist/types/resources/tools/upscale.d.ts +23 -3
- package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
- package/dist/types/types.d.ts +249 -17
- 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
|
+
}
|