@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.
- package/README.md +112 -130
- package/dist/index.js +4800 -610
- package/dist/types/client.d.ts +60 -3
- package/dist/types/http.d.ts +444 -19
- package/dist/types/index.d.ts +5 -1
- package/dist/types/internal/attachments.d.ts +17 -0
- package/dist/types/internal/helpers.d.ts +9 -0
- package/dist/types/listing.d.ts +90 -0
- package/dist/types/resources/account.d.ts +71 -5
- package/dist/types/resources/admin/authorizedApplications.d.ts +133 -0
- package/dist/types/resources/admin/chests.d.ts +51 -0
- package/dist/types/resources/admin/eventAlerts.d.ts +60 -0
- package/dist/types/resources/admin/identities.d.ts +99 -0
- package/dist/types/resources/admin/index.d.ts +77 -0
- package/dist/types/resources/admin/jobs.d.ts +97 -0
- package/dist/types/resources/admin/myOauthApplications.d.ts +282 -0
- package/dist/types/resources/admin/notepads.d.ts +49 -0
- package/dist/types/resources/admin/oauthApplications.d.ts +309 -0
- package/dist/types/resources/admin/quotas.d.ts +114 -0
- package/dist/types/resources/admin/shortLinks.d.ts +201 -0
- package/dist/types/resources/admin/types.d.ts +222 -0
- package/dist/types/resources/admin/vocalSeparations.d.ts +98 -0
- package/dist/types/resources/admin.d.ts +1841 -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 +844 -0
- package/dist/types/resources/chests.d.ts +54 -3
- package/dist/types/resources/content/analysis.d.ts +62 -0
- package/dist/types/resources/content/blogs.d.ts +471 -0
- package/dist/types/resources/content/feedbacks.d.ts +263 -0
- package/dist/types/resources/content/index.d.ts +52 -0
- package/dist/types/resources/content/intel/articles.d.ts +230 -0
- package/dist/types/resources/content/intel/config.d.ts +135 -0
- package/dist/types/resources/content/intel/index.d.ts +53 -0
- package/dist/types/resources/content/intel/items.d.ts +91 -0
- package/dist/types/resources/content/intel/reports.d.ts +108 -0
- package/dist/types/resources/content/intel/scripts.d.ts +152 -0
- package/dist/types/resources/content/intel/sources.d.ts +236 -0
- package/dist/types/resources/content/intel/stats.d.ts +105 -0
- package/dist/types/resources/content/intel/types.d.ts +86 -0
- package/dist/types/resources/content/jokes.d.ts +93 -0
- package/dist/types/resources/content/notifications.d.ts +147 -0
- package/dist/types/resources/content/serviceUsages.d.ts +73 -0
- package/dist/types/resources/content/servicesStatus.d.ts +194 -0
- package/dist/types/resources/content/siteConfig.d.ts +43 -0
- package/dist/types/resources/content/spaceInvaders.d.ts +154 -0
- package/dist/types/resources/content.d.ts +2775 -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 +69 -25
- package/dist/types/resources/library/annotations.d.ts +162 -0
- package/dist/types/resources/library/books.d.ts +534 -0
- package/dist/types/resources/library/chat.d.ts +259 -0
- package/dist/types/resources/library/index.d.ts +39 -0
- package/dist/types/resources/library/shelves.d.ts +237 -0
- package/dist/types/resources/library/types.d.ts +120 -0
- package/dist/types/resources/library.d.ts +1433 -0
- package/dist/types/resources/linkTrees.d.ts +142 -30
- package/dist/types/resources/media.d.ts +351 -0
- package/dist/types/resources/movies/addons.d.ts +388 -0
- package/dist/types/resources/movies/collections.d.ts +352 -0
- package/dist/types/resources/movies/index.d.ts +29 -0
- package/dist/types/resources/movies/types.d.ts +50 -0
- package/dist/types/resources/movies/watchProgress.d.ts +277 -0
- package/dist/types/resources/movies.d.ts +1160 -0
- package/dist/types/resources/music/artists.d.ts +1068 -0
- package/dist/types/resources/music/imports.d.ts +941 -0
- package/dist/types/resources/music/index.d.ts +61 -0
- package/dist/types/resources/music/playlists.d.ts +1027 -0
- package/dist/types/resources/music/social.d.ts +1132 -0
- package/dist/types/resources/music/songs.d.ts +1167 -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 +49 -5
- package/dist/types/resources/social/groupChats.d.ts +458 -0
- package/dist/types/resources/social/index.d.ts +19 -0
- package/dist/types/resources/social/messages.d.ts +424 -0
- package/dist/types/resources/social/relationships.d.ts +322 -0
- package/dist/types/resources/social/types.d.ts +37 -0
- package/dist/types/resources/social.d.ts +1348 -0
- package/dist/types/resources/storage/upload.d.ts +158 -11
- package/dist/types/resources/storage.d.ts +111 -27
- package/dist/types/resources/tickets.d.ts +101 -13
- 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,259 @@
|
|
|
1
|
+
/** The `library.chat` namespace: the streaming study assistant, its payloads, limits and helpers. */
|
|
2
|
+
import { OmsError } from "../../errors";
|
|
3
|
+
import { Resource } from "../../http";
|
|
4
|
+
import type { RequestOptions } from "../../types";
|
|
5
|
+
import type { BookId } from "./types";
|
|
6
|
+
/**
|
|
7
|
+
* Requests per minute allowed against `POST /books/:id/chat`.
|
|
8
|
+
*
|
|
9
|
+
* Keyed on the `Authorization` header when there is one, and on the client IP
|
|
10
|
+
* when there is not - so a cookie-authenticated web client shares one budget
|
|
11
|
+
* with every other visitor behind the same address. Over it: `429`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const BOOK_CHAT_RATE_LIMIT_PER_MINUTE = 15;
|
|
14
|
+
/**
|
|
15
|
+
* Largest chat request body the server agrees to parse: 512 KiB. Over it,
|
|
16
|
+
* `413`.
|
|
17
|
+
*
|
|
18
|
+
* This is a bound on what is PARSED, not on what reaches the model - the model
|
|
19
|
+
* caps below are applied afterwards.
|
|
20
|
+
*/
|
|
21
|
+
export declare const BOOK_CHAT_MAX_BODY_BYTES = 524288;
|
|
22
|
+
/**
|
|
23
|
+
* Characters of {@link BookChatContext.text} the assistant will read.
|
|
24
|
+
*
|
|
25
|
+
* Silently truncated server-side, not refused. Sending a whole chapter costs
|
|
26
|
+
* the bytes and buys nothing past this point.
|
|
27
|
+
*/
|
|
28
|
+
export declare const BOOK_CHAT_MAX_CONTEXT_CHARS = 12000;
|
|
29
|
+
/** Turns of history the assistant reads, newest kept. */
|
|
30
|
+
export declare const BOOK_CHAT_MAX_HISTORY_MESSAGES = 12;
|
|
31
|
+
/** Characters kept per history turn. */
|
|
32
|
+
export declare const BOOK_CHAT_MAX_MESSAGE_CHARS = 4000;
|
|
33
|
+
/** Characters kept across the whole history. */
|
|
34
|
+
export declare const BOOK_CHAT_MAX_HISTORY_CHARS = 8000;
|
|
35
|
+
/** One turn of the conversation, as the client remembers it. */
|
|
36
|
+
export interface BookChatTurn {
|
|
37
|
+
/**
|
|
38
|
+
* Only `"user"` and `"assistant"` reach the model: everything else is
|
|
39
|
+
* dropped, precisely so a client cannot inject a `"system"` turn into our
|
|
40
|
+
* own framing.
|
|
41
|
+
*/
|
|
42
|
+
readonly role: "user" | "assistant";
|
|
43
|
+
readonly content: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The passage the question is about. The CLIENT decides what that is - the
|
|
47
|
+
* current selection, the chapter on screen, or whatever its search index
|
|
48
|
+
* retrieved - and the server only frames it.
|
|
49
|
+
*/
|
|
50
|
+
export interface BookChatContext {
|
|
51
|
+
readonly kind: "selection" | "chapter" | "passages";
|
|
52
|
+
/** Chapter or section title, for the framing line. Truncated to 200 chars. */
|
|
53
|
+
readonly title?: string;
|
|
54
|
+
/** Truncated to {@link BOOK_CHAT_MAX_CONTEXT_CHARS} server-side, in silence. */
|
|
55
|
+
readonly text: string;
|
|
56
|
+
}
|
|
57
|
+
/** Arguments for {@link BookChatNamespace.stream} and {@link BookChatNamespace.ask}. */
|
|
58
|
+
export interface BookChatInput {
|
|
59
|
+
/**
|
|
60
|
+
* The whole conversation, oldest first, INCLUDING the question being asked.
|
|
61
|
+
* There is no server-side session: what you do not send did not happen.
|
|
62
|
+
*/
|
|
63
|
+
readonly messages: readonly BookChatTurn[];
|
|
64
|
+
/** The passage in front of the reader. Omit it and the assistant is told so. */
|
|
65
|
+
readonly context?: BookChatContext | null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* One decoded SSE frame from the chat stream.
|
|
69
|
+
*
|
|
70
|
+
* Exactly one field is set per frame. `delta` carries text to append, `done`
|
|
71
|
+
* marks a complete answer, and `error` is a failure that happened AFTER the
|
|
72
|
+
* `200` was written.
|
|
73
|
+
*/
|
|
74
|
+
export interface BookChatEvent {
|
|
75
|
+
readonly delta?: string;
|
|
76
|
+
readonly done?: boolean;
|
|
77
|
+
readonly error?: string;
|
|
78
|
+
}
|
|
79
|
+
/** Why a chat stream failed. See {@link BookChatError}. */
|
|
80
|
+
export type BookChatFailureReason =
|
|
81
|
+
/** The server wrote an `error` frame: generation failed after the 200. */
|
|
82
|
+
"assistant_unavailable"
|
|
83
|
+
/** The stream ended without a `done` frame, so the answer is cut short. */
|
|
84
|
+
| "truncated";
|
|
85
|
+
/**
|
|
86
|
+
* The chat stream failed AFTER the response status was written.
|
|
87
|
+
*
|
|
88
|
+
* This is not an {@link OmsApiError}, and that is the whole point of it
|
|
89
|
+
* existing. The server writes `200` and the SSE headers before the
|
|
90
|
+
* model has produced a single token, so anything that goes wrong from then on -
|
|
91
|
+
* the inference sidecar erroring, the proxy dropping a stream that went quiet -
|
|
92
|
+
* arrives INSIDE a successful response. Code that decides success by status
|
|
93
|
+
* alone reports a truncated answer, or an empty one, as a complete reply.
|
|
94
|
+
*
|
|
95
|
+
* `code` is `"server_error"`, so `error.retryable` is `true`: asking again is
|
|
96
|
+
* the right move for both reasons, subject to
|
|
97
|
+
* {@link BOOK_CHAT_RATE_LIMIT_PER_MINUTE}.
|
|
98
|
+
*/
|
|
99
|
+
export declare class BookChatError extends OmsError {
|
|
100
|
+
static readonly errorName: string;
|
|
101
|
+
/** Which of the two post-200 failures this is. */
|
|
102
|
+
readonly reason: BookChatFailureReason;
|
|
103
|
+
/** Text already yielded before the failure. Usually worth showing, marked as incomplete. */
|
|
104
|
+
readonly partial: string;
|
|
105
|
+
constructor(message: string, reason: BookChatFailureReason, partial: string, url?: string);
|
|
106
|
+
toJSON(): Record<string, unknown>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* True when this error is the assistant refusing because every generation slot
|
|
110
|
+
* is taken.
|
|
111
|
+
*
|
|
112
|
+
* The server fails closed rather than queueing - a caller parked waiting for
|
|
113
|
+
* a slot would be holding the very server thread the slots exist to protect -
|
|
114
|
+
* so the answer is `503` with `"The assistant is busy, try again in a moment"`.
|
|
115
|
+
* It is transient and it is NOT a rate limit: the caller did nothing wrong and
|
|
116
|
+
* `Retry-After` is not sent, so back off on your own and try again.
|
|
117
|
+
*/
|
|
118
|
+
export declare function isBookChatBusy(error: unknown): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Whether {@link BookChatNamespace.stream} will deliver this answer in pieces
|
|
121
|
+
* on THIS runtime.
|
|
122
|
+
*
|
|
123
|
+
* `true` in a browser and in Bun, `false` in React Native, whose `fetch` hands
|
|
124
|
+
* the whole body over at the end and has no `ReadableStream` at all. Both
|
|
125
|
+
* paths reach the same endpoint and produce the same final text; only the
|
|
126
|
+
* arrival differs, and this is the question to ask before a UI promises a
|
|
127
|
+
* typing indicator. A typing indicator that never types is worse than a
|
|
128
|
+
* spinner that admits it is waiting.
|
|
129
|
+
*
|
|
130
|
+
* Read it at the point of use, never cached at module load: a host may install
|
|
131
|
+
* a polyfill after the SDK is imported.
|
|
132
|
+
*/
|
|
133
|
+
export declare function bookChatIsIncremental(): boolean;
|
|
134
|
+
/** Options for {@link BookChatNamespace.stream} and {@link BookChatNamespace.ask}. */
|
|
135
|
+
export interface BookChatStreamOptions extends RequestOptions {
|
|
136
|
+
/**
|
|
137
|
+
* How long to wait for the NEXT piece of the answer before giving up, in
|
|
138
|
+
* milliseconds. Defaults to the transport's 45 seconds; `0` disables it.
|
|
139
|
+
*
|
|
140
|
+
* A silence limit, not a total: an answer that keeps producing runs as long
|
|
141
|
+
* as it likes. It exists because a stalled sidecar once answered `200` and
|
|
142
|
+
* then said nothing for two minutes at a time, and `await reader.read()` has
|
|
143
|
+
* no deadline of its own, so the chat panel span for as long as the tab
|
|
144
|
+
* stayed open. `timeoutMs` cannot cover this: it is disposed the moment the
|
|
145
|
+
* headers arrive, or no stream could outlive it.
|
|
146
|
+
*
|
|
147
|
+
* It has to sit well clear of a cold model's first token - the 8B model has
|
|
148
|
+
* to be paged in before it can answer - which is why 45 seconds and not five.
|
|
149
|
+
*
|
|
150
|
+
* IGNORED on the buffered path (React Native), where there is one read and
|
|
151
|
+
* the caller's `signal` is what bounds it.
|
|
152
|
+
*/
|
|
153
|
+
readonly silenceTimeoutMs?: number;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The `library.chat` namespace: the reader's study assistant.
|
|
157
|
+
*
|
|
158
|
+
* `POST /books/:id/chat` is the only STREAMING endpoint in the whole API.
|
|
159
|
+
*
|
|
160
|
+
* ## What it costs, and why it refuses rather than queues
|
|
161
|
+
*
|
|
162
|
+
* One request parks a server thread for the whole generation, tens of seconds
|
|
163
|
+
* on the local model. The server caps how many generations run at once and
|
|
164
|
+
* FAILS CLOSED when they are all taken - a caller parked waiting for a slot
|
|
165
|
+
* would be holding the very thread the slots exist to protect. That refusal is
|
|
166
|
+
* a `503`; see {@link isBookChatBusy}. On top of it, one client may only START
|
|
167
|
+
* {@link BOOK_CHAT_RATE_LIMIT_PER_MINUTE} generations a minute, so a loop
|
|
168
|
+
* cannot take whatever slot frees up next.
|
|
169
|
+
*
|
|
170
|
+
* ## The client owns the conversation
|
|
171
|
+
*
|
|
172
|
+
* There is no server-side session and no history storage. Every request carries
|
|
173
|
+
* the whole conversation and the passage the question is about; what you do not
|
|
174
|
+
* send did not happen. The server owns the framing and the caps, and it drops
|
|
175
|
+
* any role other than `user` and `assistant` so a client cannot inject a
|
|
176
|
+
* `system` turn into our own prompt.
|
|
177
|
+
*/
|
|
178
|
+
export declare class BookChatNamespace extends Resource {
|
|
179
|
+
/**
|
|
180
|
+
* `POST /books/:id/chat` - asks the assistant and yields the answer as it
|
|
181
|
+
* arrives.
|
|
182
|
+
*
|
|
183
|
+
* Yields TEXT DELTAS, already unwrapped from their SSE frames: concatenating
|
|
184
|
+
* everything this yields is the complete answer. Framing is handled here
|
|
185
|
+
* because a chunk is not a frame - a `data:` line can arrive split across two
|
|
186
|
+
* network chunks - and every client that tried to parse the raw stream itself
|
|
187
|
+
* got that wrong at least once.
|
|
188
|
+
*
|
|
189
|
+
* ## The two runtimes do not deliver the same way, and the difference is real
|
|
190
|
+
*
|
|
191
|
+
* - **browser and Bun**: one yield per delta, as the model produces it. A
|
|
192
|
+
* typing UI works, and abandoning the loop (a `break`, or an early
|
|
193
|
+
* `return`) cancels the reader, closes the connection, and the server
|
|
194
|
+
* notices the disconnect and RELEASES ITS GENERATION SLOT. Stopping early
|
|
195
|
+
* genuinely stops the work.
|
|
196
|
+
* - **React Native**: ONE yield, containing the entire answer, after the
|
|
197
|
+
* server has finished generating. RN's `fetch` is `XMLHttpRequest` under a
|
|
198
|
+
* shim, the whole body is accumulated natively and handed over at the end,
|
|
199
|
+
* and `response.body` does not exist. This is not a gap to polyfill.
|
|
200
|
+
* Consequences to design around: a typing indicator will sit still and then
|
|
201
|
+
* snap to the finished answer, so ask {@link bookChatIsIncremental} and
|
|
202
|
+
* show a spinner instead; and `break`ing out of the loop cancels NOTHING,
|
|
203
|
+
* because the request already completed - only an `AbortSignal` can stop
|
|
204
|
+
* the work, and only by tearing down the whole request.
|
|
205
|
+
*
|
|
206
|
+
* The final text is identical on both. Nothing is lost, nothing is
|
|
207
|
+
* reordered, and no chunk boundary is invented to make one look like the
|
|
208
|
+
* other.
|
|
209
|
+
*
|
|
210
|
+
* ## A `200` does not mean the answer worked
|
|
211
|
+
*
|
|
212
|
+
* The status and the SSE headers are written before the model has produced a
|
|
213
|
+
* token, so every later failure is in-band. Two of them, both raised as
|
|
214
|
+
* {@link BookChatError} with the text already yielded attached:
|
|
215
|
+
*
|
|
216
|
+
* - an `error` frame (`reason: "assistant_unavailable"`) - the inference
|
|
217
|
+
* client raised, and the upstream message is deliberately withheld because
|
|
218
|
+
* it can carry the sidecar's internals;
|
|
219
|
+
* - the stream ending without a `done` frame (`reason: "truncated"`) - the
|
|
220
|
+
* proxy dropped a stream that went quiet, or the connection was cut
|
|
221
|
+
* mid-answer. Returning quietly here would report a half-answer as a
|
|
222
|
+
* complete one, which is the whole reason the `done` frame exists.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```ts
|
|
226
|
+
* let answer = "";
|
|
227
|
+
* for await (const delta of oms.library.chat.stream(bookId, { messages, context })) {
|
|
228
|
+
* answer += delta;
|
|
229
|
+
* render(answer); // one paint per delta, or one paint on RN
|
|
230
|
+
* }
|
|
231
|
+
* ```
|
|
232
|
+
*
|
|
233
|
+
* @throws {OmsAuthError} 401 `"Session required"` when anonymous.
|
|
234
|
+
* @throws {OmsApiError} 404 `"Book not found"` when the book is not viewable;
|
|
235
|
+
* 413 `"Request too big"` over {@link BOOK_CHAT_MAX_BODY_BYTES}; 429 over
|
|
236
|
+
* {@link BOOK_CHAT_RATE_LIMIT_PER_MINUTE}; 503 when every generation slot
|
|
237
|
+
* is taken ({@link isBookChatBusy}).
|
|
238
|
+
* @throws {BookChatError} for either post-`200` failure. Check `reason` and
|
|
239
|
+
* `partial`.
|
|
240
|
+
* @throws {OmsTimeoutError} when the stream goes quiet for
|
|
241
|
+
* {@link BookChatStreamOptions.silenceTimeoutMs}. Streaming path only.
|
|
242
|
+
*/
|
|
243
|
+
stream(bookId: BookId, input: BookChatInput, options?: BookChatStreamOptions): AsyncGenerator<string, void, undefined>;
|
|
244
|
+
/**
|
|
245
|
+
* The same call as {@link stream}, waited out and handed back as one string.
|
|
246
|
+
*
|
|
247
|
+
* For anywhere a partial answer has nowhere to go. It is exactly `stream`
|
|
248
|
+
* drained into a buffer, so it raises the same
|
|
249
|
+
* errors for the same reasons - including {@link BookChatError} on a
|
|
250
|
+
* truncated stream, which is the case a naive `await response.text()` would
|
|
251
|
+
* hand back as a successful half-answer.
|
|
252
|
+
*
|
|
253
|
+
* It does NOT make the request cheaper or faster anywhere. On a browser and
|
|
254
|
+
* on Bun the answer still streams in and is merely accumulated here; on React
|
|
255
|
+
* Native it was going to arrive in one piece regardless. Choose it because
|
|
256
|
+
* the CALLER has no use for pieces, never as a way to avoid streaming.
|
|
257
|
+
*/
|
|
258
|
+
ask(bookId: BookId, input: BookChatInput, options?: BookChatStreamOptions): Promise<string>;
|
|
259
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The `library` domain: books, shelves, annotations and the study assistant, reachable as `oms.library`. */
|
|
2
|
+
import { Resource } from "../../http";
|
|
3
|
+
import type { RequestOptions } from "../../types";
|
|
4
|
+
import { LibraryAnnotationsNamespace } from "./annotations";
|
|
5
|
+
import { LibraryBooksNamespace } from "./books";
|
|
6
|
+
import { BookChatNamespace } from "./chat";
|
|
7
|
+
import { LibraryShelvesNamespace } from "./shelves";
|
|
8
|
+
import type { PublicLibrary } from "./types";
|
|
9
|
+
export * from "./types";
|
|
10
|
+
export * from "./books";
|
|
11
|
+
export * from "./shelves";
|
|
12
|
+
export * from "./annotations";
|
|
13
|
+
export * from "./chat";
|
|
14
|
+
/**
|
|
15
|
+
* The `library` namespace, reachable as `oms.library`.
|
|
16
|
+
*
|
|
17
|
+
* A thin holder for the four sub-namespaces. Each of them is exported on its
|
|
18
|
+
* own too, so a host that would rather write `oms.books` can construct
|
|
19
|
+
* {@link LibraryBooksNamespace} directly with the same `ApiClient`.
|
|
20
|
+
*/
|
|
21
|
+
export declare class LibraryNamespace extends Resource {
|
|
22
|
+
/** Books: upload, browse, share, read the bytes. */
|
|
23
|
+
readonly books: LibraryBooksNamespace;
|
|
24
|
+
/** Shelves: named, ordered, shareable groups of your own books. */
|
|
25
|
+
readonly shelves: LibraryShelvesNamespace;
|
|
26
|
+
/** Highlights, notes, bookmarks and reading progress. Always private. */
|
|
27
|
+
readonly annotations: LibraryAnnotationsNamespace;
|
|
28
|
+
/** The streaming study assistant. */
|
|
29
|
+
readonly chat: BookChatNamespace;
|
|
30
|
+
/**
|
|
31
|
+
* `GET /books/libraries` - the public-library directory.
|
|
32
|
+
*
|
|
33
|
+
* Delegates to {@link LibraryBooksNamespace.libraries}. It lives on the books
|
|
34
|
+
* route because that is where Rails mounted it, and it is surfaced here
|
|
35
|
+
* because it is a directory of READERS, which is what a caller looking for it
|
|
36
|
+
* expects `oms.library` to have.
|
|
37
|
+
*/
|
|
38
|
+
publicLibraries(options?: RequestOptions): Promise<PublicLibrary[]>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/** The `library.shelves` namespace, with the record, inputs and limits only it uses. */
|
|
2
|
+
import { Resource } from "../../http";
|
|
3
|
+
import type { ListParams } from "../../listing";
|
|
4
|
+
import type { Id, Paginated, RequestOptions, Timestamp } from "../../types";
|
|
5
|
+
import type { Book, BookId, BookVisibility } from "./types";
|
|
6
|
+
/** Primary key of a shelf. A number, like every id in this file. */
|
|
7
|
+
export type BookShelfId = number;
|
|
8
|
+
/** Longest shelf name accepted. */
|
|
9
|
+
export declare const BOOK_SHELF_NAME_MAX_LENGTH = 120;
|
|
10
|
+
/** Longest shelf description accepted. */
|
|
11
|
+
export declare const BOOK_SHELF_DESCRIPTION_MAX_LENGTH = 1000;
|
|
12
|
+
/**
|
|
13
|
+
* Most shelves one user may own.
|
|
14
|
+
*
|
|
15
|
+
* Checked on creation only, so the 101st shelf fails with a `400` carrying
|
|
16
|
+
* `"You have reached the maximum of 100 shelves"`; existing shelves keep saving.
|
|
17
|
+
*/
|
|
18
|
+
export declare const BOOK_SHELVES_MAX_PER_USER = 100;
|
|
19
|
+
/**
|
|
20
|
+
* A shelf: a named, ordered group of one reader's own books.
|
|
21
|
+
*
|
|
22
|
+
* `books` is present ONLY on the `:extended` view, which means
|
|
23
|
+
* {@link LibraryShelvesNamespace.get} and the three arrange calls carry it and
|
|
24
|
+
* {@link LibraryShelvesNamespace.list} does not. That is not an oversight to
|
|
25
|
+
* work around by listing and hoping - it is why `get` exists.
|
|
26
|
+
*/
|
|
27
|
+
export interface BookShelf {
|
|
28
|
+
readonly id: BookShelfId;
|
|
29
|
+
readonly created_at: Timestamp;
|
|
30
|
+
readonly updated_at: Timestamp;
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly description: string | null;
|
|
33
|
+
readonly visibility: BookVisibility;
|
|
34
|
+
/** The owner's ordering of their own shelves. Ascending. */
|
|
35
|
+
readonly position: number;
|
|
36
|
+
/** Owner. A string id. */
|
|
37
|
+
readonly user_id: Id;
|
|
38
|
+
readonly owner_handle: string | null;
|
|
39
|
+
readonly owner_name: string | null;
|
|
40
|
+
/** Whether the ASKING user may edit the shelf. */
|
|
41
|
+
readonly editable: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* How many of this shelf's books the ASKING user may actually open, which is
|
|
44
|
+
* not how many are on it: a shelf can hold a book its owner later made
|
|
45
|
+
* private again, and counting those would promise a stranger rows they
|
|
46
|
+
* cannot see. So `book_count` can be smaller than `books.length` would be
|
|
47
|
+
* for the owner, and it costs one extra query PER SHELF in a listing.
|
|
48
|
+
*/
|
|
49
|
+
readonly book_count: number;
|
|
50
|
+
/**
|
|
51
|
+
* The shelf's books, in `position` order, filtered to what the asking user
|
|
52
|
+
* may see. Present on the `:extended` view only - see the note above.
|
|
53
|
+
*/
|
|
54
|
+
readonly books?: Book[];
|
|
55
|
+
}
|
|
56
|
+
/** Filter columns of `GET /book_shelves`, on top of {@link BASE_FILTER_COLUMNS}. */
|
|
57
|
+
export declare const BOOK_SHELF_FILTER_COLUMNS: readonly ["user_id", "visibility"];
|
|
58
|
+
/** Arguments for {@link LibraryShelvesNamespace.list}. */
|
|
59
|
+
export interface ListBookShelvesParams extends ListParams<(typeof BOOK_SHELF_FILTER_COLUMNS)[number]> {
|
|
60
|
+
/** Exact owner id. The only way to ask for one person's shelves. */
|
|
61
|
+
readonly userId?: Id;
|
|
62
|
+
/** Exact visibility. */
|
|
63
|
+
readonly visibility?: BookVisibility;
|
|
64
|
+
/** Exact ids. An array becomes `IN (...)`. */
|
|
65
|
+
readonly ids?: readonly BookShelfId[];
|
|
66
|
+
/** `"column:asc"` / `"column:desc"`. Defaults to the endpoint's own ordering. */
|
|
67
|
+
readonly order?: string;
|
|
68
|
+
}
|
|
69
|
+
/** Arguments for {@link LibraryShelvesNamespace.create}. */
|
|
70
|
+
export interface CreateBookShelfInput {
|
|
71
|
+
readonly name: string;
|
|
72
|
+
readonly description?: string | null;
|
|
73
|
+
/** Defaults to `"private"` server-side. */
|
|
74
|
+
readonly visibility?: BookVisibility;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Arguments for {@link LibraryShelvesNamespace.update}.
|
|
78
|
+
*
|
|
79
|
+
* `position` is writable here and nowhere else: it orders the owner's SHELVES
|
|
80
|
+
* against each other, and has nothing to do with the order of books ON a shelf
|
|
81
|
+
* (that is {@link LibraryShelvesNamespace.reorder}).
|
|
82
|
+
*/
|
|
83
|
+
export interface UpdateBookShelfInput {
|
|
84
|
+
readonly name?: string;
|
|
85
|
+
readonly description?: string | null;
|
|
86
|
+
readonly visibility?: BookVisibility;
|
|
87
|
+
readonly position?: number;
|
|
88
|
+
}
|
|
89
|
+
/** The `library.shelves` namespace, reachable as `oms.library.shelves`. */
|
|
90
|
+
export declare class LibraryShelvesNamespace extends Resource {
|
|
91
|
+
/**
|
|
92
|
+
* `GET /book_shelves` - browse shelves.
|
|
93
|
+
*
|
|
94
|
+
* Same two-axis visibility rule as books: every `public` shelf plus, for a
|
|
95
|
+
* signed-in caller, their own whatever their visibility. An `unlisted`
|
|
96
|
+
* shelf opens by its link and appears in no listing.
|
|
97
|
+
*
|
|
98
|
+
* Ordered by `(position, created_at)` unless you override it, so the owner's
|
|
99
|
+
* own arrangement is the default and a paged walk is stable without asking.
|
|
100
|
+
*
|
|
101
|
+
* **The rows here carry no `books`.** `index` renders the default view and
|
|
102
|
+
* only the `:extended` one inlines the association. Every other method in
|
|
103
|
+
* this namespace returns the extended shape; this one does not. Do not build
|
|
104
|
+
* a shelf screen by listing and reading `books` - it is `undefined`.
|
|
105
|
+
*
|
|
106
|
+
* Note also that `book_count` costs one query per shelf (per row, because
|
|
107
|
+
* it depends on who is asking). A 100-shelf page is 100 extra counts. Page
|
|
108
|
+
* it.
|
|
109
|
+
*
|
|
110
|
+
* @throws {OmsApiError} 400 naming the offending key when a filter is not
|
|
111
|
+
* one of `user_id`, `visibility`, `id`, `created_at`, `updated_at`. There
|
|
112
|
+
* are no `extra_options` on this endpoint at all, so ANY key in that
|
|
113
|
+
* bucket is a 400.
|
|
114
|
+
*/
|
|
115
|
+
list(params?: ListBookShelvesParams, options?: RequestOptions): Promise<Paginated<BookShelf>>;
|
|
116
|
+
/**
|
|
117
|
+
* `GET /book_shelves/:id` - one shelf, WITH its books.
|
|
118
|
+
*
|
|
119
|
+
* The `:extended` view, so {@link BookShelf.books} is populated here and
|
|
120
|
+
* nowhere in {@link list}. The books are filtered to what the ASKING user may
|
|
121
|
+
* open, in shelf order: a shelf that holds a book its owner later made
|
|
122
|
+
* private again shows that book to the owner and silently omits it for
|
|
123
|
+
* everyone else, which is also why `book_count` and `books.length` can
|
|
124
|
+
* disagree between two viewers of the same shelf.
|
|
125
|
+
*
|
|
126
|
+
* Works anonymously, through the wider visibility scope, which is how an
|
|
127
|
+
* `unlisted` shelf link resolves.
|
|
128
|
+
*
|
|
129
|
+
* @throws {OmsApiError} 404 for an unknown id and for someone else's private
|
|
130
|
+
* shelf, indistinguishably.
|
|
131
|
+
*/
|
|
132
|
+
get(id: BookShelfId, options?: RequestOptions): Promise<BookShelf>;
|
|
133
|
+
/**
|
|
134
|
+
* `POST /book_shelves` - creates an empty shelf owned by the caller.
|
|
135
|
+
*
|
|
136
|
+
* The owner is taken from the session and cannot be sent. Visibility
|
|
137
|
+
* defaults to `private`.
|
|
138
|
+
*
|
|
139
|
+
* Not retried by default, like every create: a replay after a lost answer
|
|
140
|
+
* makes a second identical shelf, and shelves are capped per user.
|
|
141
|
+
*
|
|
142
|
+
* @throws {OmsAuthError} 401 when anonymous.
|
|
143
|
+
* @throws {OmsApiError} 400 `"You have reached the maximum of 100 shelves"`
|
|
144
|
+
* at {@link BOOK_SHELVES_MAX_PER_USER}, or the model's validation messages
|
|
145
|
+
* for a missing or over-long name.
|
|
146
|
+
*/
|
|
147
|
+
create(input: CreateBookShelfInput, options?: RequestOptions): Promise<BookShelf>;
|
|
148
|
+
/**
|
|
149
|
+
* `PATCH /book_shelves/:id` - renames, re-describes, re-shares, or moves the
|
|
150
|
+
* shelf among the owner's other shelves.
|
|
151
|
+
*
|
|
152
|
+
* {@link UpdateBookShelfInput.position} orders SHELVES, not the books on one.
|
|
153
|
+
* Nothing renumbers the others, so two shelves can share a position and the
|
|
154
|
+
* `(position, created_at)` sort then decides between them by age.
|
|
155
|
+
*
|
|
156
|
+
* @throws {OmsAuthError} 401 when the shelf is not yours.
|
|
157
|
+
* @throws {OmsApiError} 404 for an unknown id or someone else's private
|
|
158
|
+
* shelf; 400 with the model's validation messages.
|
|
159
|
+
*/
|
|
160
|
+
update(id: BookShelfId, input: UpdateBookShelfInput, options?: RequestOptions): Promise<BookShelf>;
|
|
161
|
+
/**
|
|
162
|
+
* `DELETE /book_shelves/:id` - removes the shelf.
|
|
163
|
+
*
|
|
164
|
+
* The BOOKS SURVIVE. Only the `book_shelf_items` join rows are destroyed, so
|
|
165
|
+
* this un-files the books rather than deleting them - the opposite of
|
|
166
|
+
* {@link LibraryBooksNamespace.delete}, and worth saying in the confirmation
|
|
167
|
+
* dialog.
|
|
168
|
+
*
|
|
169
|
+
* @throws {OmsAuthError} 401 when the shelf is not yours.
|
|
170
|
+
* @throws {OmsApiError} 404 the second time round.
|
|
171
|
+
*/
|
|
172
|
+
delete(id: BookShelfId, options?: RequestOptions): Promise<void>;
|
|
173
|
+
/**
|
|
174
|
+
* `POST /book_shelves/:id/add_book` - puts one of YOUR books on the shelf.
|
|
175
|
+
*
|
|
176
|
+
* IDEMPOTENT: adding a book that is already there is a no-op with a `200`,
|
|
177
|
+
* not a duplicate and not a 422. Position is assigned automatically at the
|
|
178
|
+
* end of the shelf.
|
|
179
|
+
*
|
|
180
|
+
* The book must be the shelf owner's own: somebody else's book - public or
|
|
181
|
+
* not - is a `404 "Book not found"` rather than a permission error, and a
|
|
182
|
+
* shelf can never hold a book whose owner might later make it private and
|
|
183
|
+
* leak it through the shelf's visibility.
|
|
184
|
+
*
|
|
185
|
+
* Answers the whole shelf in its `:extended` view, so the response already
|
|
186
|
+
* contains the new arrangement and there is no need to re-fetch.
|
|
187
|
+
*
|
|
188
|
+
* @throws {OmsApiError} 404 `"Shelf not found"` for an unknown shelf,
|
|
189
|
+
* `"Book not found"` for a book that is not yours; 403 `"Not your shelf"`
|
|
190
|
+
* for someone else's shelf - note this action answers **403** where
|
|
191
|
+
* {@link update} answers 401 for the same offence.
|
|
192
|
+
* @throws {OmsApiError} 422 with the item's validation messages, which in
|
|
193
|
+
* practice only fires if the ownership check is ever relaxed.
|
|
194
|
+
*/
|
|
195
|
+
addBook(id: BookShelfId, bookId: BookId, options?: RequestOptions): Promise<BookShelf>;
|
|
196
|
+
/**
|
|
197
|
+
* `DELETE /book_shelves/:id/remove_book` - takes a book off the shelf.
|
|
198
|
+
*
|
|
199
|
+
* The book itself is untouched; only the join row goes.
|
|
200
|
+
*
|
|
201
|
+
* IDEMPOTENT to the point of being silent: removing a book that was never
|
|
202
|
+
* on the shelf answers `200` with the unchanged shelf. There is no way to
|
|
203
|
+
* tell the two
|
|
204
|
+
* apart from the response - compare `book_count` yourself if you need to.
|
|
205
|
+
*
|
|
206
|
+
* The book id travels as a QUERY PARAMETER rather than in a body. The server
|
|
207
|
+
* reads it from either, and a `DELETE` with a body is the shape
|
|
208
|
+
* that intermediaries are least reliable about forwarding; the transport
|
|
209
|
+
* also has no body slot on `delete`. Nothing here is a filter bucket, so the
|
|
210
|
+
* unknown-key guard does not apply.
|
|
211
|
+
*
|
|
212
|
+
* @throws {OmsApiError} 404 `"Shelf not found"`; 403 `"Not your shelf"`.
|
|
213
|
+
*/
|
|
214
|
+
removeBook(id: BookShelfId, bookId: BookId, options?: RequestOptions): Promise<BookShelf>;
|
|
215
|
+
/**
|
|
216
|
+
* `POST /book_shelves/:id/reorder` - sets the order of the books on a shelf.
|
|
217
|
+
*
|
|
218
|
+
* SEND THE COMPLETE LIST, in the order you want, every time. The action
|
|
219
|
+
* writes `position = index + 1` for each id it was given and touches nothing
|
|
220
|
+
* else, so a partial list leaves the omitted books on their OLD positions -
|
|
221
|
+
* which now collide with the new ones, and the shelf comes back interleaved
|
|
222
|
+
* in an order nobody chose. There is no error: it is a `200` and a shelf that
|
|
223
|
+
* looks shuffled.
|
|
224
|
+
*
|
|
225
|
+
* Ids that are not on this shelf are ignored in silence, and so are ids of
|
|
226
|
+
* books that do not exist. So this cannot
|
|
227
|
+
* be used to add a book - use {@link addBook} first, then reorder.
|
|
228
|
+
*
|
|
229
|
+
* The whole thing runs in one transaction, so a shelf is never left half
|
|
230
|
+
* renumbered.
|
|
231
|
+
*
|
|
232
|
+
* @throws {TypeError} before any request for an empty list, which the server
|
|
233
|
+
* would answer `400 "No order given"`.
|
|
234
|
+
* @throws {OmsApiError} 404 `"Shelf not found"`; 403 `"Not your shelf"`.
|
|
235
|
+
*/
|
|
236
|
+
reorder(id: BookShelfId, bookIds: readonly BookId[], options?: RequestOptions): Promise<BookShelf>;
|
|
237
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Identifiers, enumerations and records shared by every library namespace. */
|
|
2
|
+
import type { Id, Timestamp } from "../../types";
|
|
3
|
+
/**
|
|
4
|
+
* Primary key of a book. A NUMBER: `books` never moved to the opaque string
|
|
5
|
+
* ids the account-side tables use, even though its `user_id` and its three
|
|
6
|
+
* `*_fs_node_id` columns are strings.
|
|
7
|
+
*/
|
|
8
|
+
export type BookId = number;
|
|
9
|
+
/** Primary key of an annotation. A number. */
|
|
10
|
+
export type BookAnnotationId = number;
|
|
11
|
+
/** The two file formats the library accepts. */
|
|
12
|
+
export declare const BOOK_FORMATS: readonly ["pdf", "epub"];
|
|
13
|
+
/** One of {@link BOOK_FORMATS}. */
|
|
14
|
+
export type BookFormat = (typeof BOOK_FORMATS)[number];
|
|
15
|
+
/**
|
|
16
|
+
* Visibility of a book or a shelf. The same three levels apply to both.
|
|
17
|
+
*
|
|
18
|
+
* - `private` - owner only.
|
|
19
|
+
* - `unlisted` - anyone holding the link. Reachable by `GET /books/:id`,
|
|
20
|
+
* absent from every listing. This is the level a "share" button sets.
|
|
21
|
+
* - `public` - anyone, AND listed in the owner's library and in explore.
|
|
22
|
+
*/
|
|
23
|
+
export declare const BOOK_VISIBILITIES: readonly ["private", "unlisted", "public"];
|
|
24
|
+
/** One of {@link BOOK_VISIBILITIES}. */
|
|
25
|
+
export type BookVisibility = (typeof BOOK_VISIBILITIES)[number];
|
|
26
|
+
/** Kinds of annotation. */
|
|
27
|
+
export declare const BOOK_ANNOTATION_KINDS: readonly ["highlight", "note", "bookmark", "progress"];
|
|
28
|
+
/** One of {@link BOOK_ANNOTATION_KINDS}. */
|
|
29
|
+
export type BookAnnotationKind = (typeof BOOK_ANNOTATION_KINDS)[number];
|
|
30
|
+
/**
|
|
31
|
+
* A book.
|
|
32
|
+
*
|
|
33
|
+
* `editable` is computed against the ASKING user, which is what lets one
|
|
34
|
+
* listing mix your own books with strangers' public ones and still know which
|
|
35
|
+
* ones offer an edit affordance. It is also the answer for "may I delete
|
|
36
|
+
* this".
|
|
37
|
+
*/
|
|
38
|
+
export interface Book {
|
|
39
|
+
/** Integer primary key. See {@link BookId}. */
|
|
40
|
+
readonly id: BookId;
|
|
41
|
+
readonly created_at: Timestamp;
|
|
42
|
+
readonly updated_at: Timestamp;
|
|
43
|
+
readonly title: string;
|
|
44
|
+
readonly author: string | null;
|
|
45
|
+
readonly description: string | null;
|
|
46
|
+
/** Detected at upload from the extension or the content type, never from the caller. */
|
|
47
|
+
readonly format: BookFormat;
|
|
48
|
+
readonly isbn: string | null;
|
|
49
|
+
readonly language: string | null;
|
|
50
|
+
readonly page_count: number | null;
|
|
51
|
+
/** Size of the stored file. `null` only on rows that predate the column. */
|
|
52
|
+
readonly file_size_bytes: number | null;
|
|
53
|
+
/** Always an array, never `null`: the column is `jsonb NOT NULL DEFAULT '[]'`. */
|
|
54
|
+
readonly tags: string[];
|
|
55
|
+
readonly visibility: BookVisibility;
|
|
56
|
+
/** Owner. A string id, next to an integer `id`. */
|
|
57
|
+
readonly user_id: Id;
|
|
58
|
+
/** Storage node of the cover the owner uploaded. A STRING id. */
|
|
59
|
+
readonly cover_fs_node_id: Id | null;
|
|
60
|
+
/** Storage node of the WebP thumbnail the server derived from it. A STRING id. */
|
|
61
|
+
readonly compressed_cover_fs_node_id: Id | null;
|
|
62
|
+
/** True when either cover node is set. Branch on this, not on the two ids. */
|
|
63
|
+
readonly has_cover: boolean;
|
|
64
|
+
readonly owner_handle: string | null;
|
|
65
|
+
readonly owner_name: string | null;
|
|
66
|
+
/** Whether the ASKING user may edit the book. Also answers "may I delete this". */
|
|
67
|
+
readonly editable: boolean;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* One row of the public-library directory: a person, not a book.
|
|
71
|
+
*
|
|
72
|
+
* Unlike every other payload in the namespace it carries no `id`, no
|
|
73
|
+
* `created_at` and no `updated_at`, and the owner is addressed by `handle`.
|
|
74
|
+
* Feed that handle back as {@link ListBooksParams.ownerHandle}.
|
|
75
|
+
*/
|
|
76
|
+
export interface PublicLibrary {
|
|
77
|
+
readonly handle: string;
|
|
78
|
+
readonly name: string;
|
|
79
|
+
/** What the owner named their library, or `null` if they never did. */
|
|
80
|
+
readonly library_name: string | null;
|
|
81
|
+
readonly library_description: string | null;
|
|
82
|
+
/** PUBLIC books only. An owner's unlisted and private books are not counted. */
|
|
83
|
+
readonly book_count: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Where in the book an annotation sits. Format-specific and opaque to the API:
|
|
87
|
+
* the column is `jsonb NOT NULL DEFAULT '{}'` and nothing server-side reads
|
|
88
|
+
* inside it.
|
|
89
|
+
*
|
|
90
|
+
* The two shapes the readers agree on today:
|
|
91
|
+
* - EPUB: `{ cfiRange }` for a range, `{ cfi }` for a point;
|
|
92
|
+
* - PDF: `{ pageNumber, rects: [{ x, y, w, h }] }`, page-normalised to `[0,1]`
|
|
93
|
+
* so a highlight survives a zoom.
|
|
94
|
+
*
|
|
95
|
+
* A new reader may write a new shape. Read defensively.
|
|
96
|
+
*/
|
|
97
|
+
export type BookAnnotationLocation = Record<string, unknown>;
|
|
98
|
+
/**
|
|
99
|
+
* A highlight, a note, a bookmark, or a reading-progress marker.
|
|
100
|
+
*
|
|
101
|
+
* ALWAYS PRIVATE to the reader who made it, even on a public book: an
|
|
102
|
+
* anonymous caller sees none, and a signed-in caller sees only their own.
|
|
103
|
+
* There is no sharing of highlights, and `GET /book_annotations` therefore
|
|
104
|
+
* needs no user filter - it can only ever return your own.
|
|
105
|
+
*/
|
|
106
|
+
export interface BookAnnotation {
|
|
107
|
+
readonly id: BookAnnotationId;
|
|
108
|
+
readonly created_at: Timestamp;
|
|
109
|
+
readonly updated_at: Timestamp;
|
|
110
|
+
readonly book_id: BookId;
|
|
111
|
+
/** Author. Always the caller, since nobody else's are visible. */
|
|
112
|
+
readonly user_id: Id;
|
|
113
|
+
readonly kind: BookAnnotationKind;
|
|
114
|
+
readonly location: BookAnnotationLocation;
|
|
115
|
+
/** Free-form, usually a CSS colour. Not validated server-side. */
|
|
116
|
+
readonly color: string | null;
|
|
117
|
+
readonly note: string | null;
|
|
118
|
+
/** The passage the reader selected, kept so a highlight can be listed without opening the book. */
|
|
119
|
+
readonly selected_text: string | null;
|
|
120
|
+
}
|