@omelhorsite/sdk 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +112 -130
  2. package/dist/index.js +1045 -1242
  3. package/dist/types/index.d.ts +1 -0
  4. package/dist/types/internal/attachments.d.ts +17 -0
  5. package/dist/types/internal/helpers.d.ts +9 -0
  6. package/dist/types/listing.d.ts +90 -0
  7. package/dist/types/resources/account.d.ts +5 -2
  8. package/dist/types/resources/admin/authorizedApplications.d.ts +133 -0
  9. package/dist/types/resources/admin/chests.d.ts +51 -0
  10. package/dist/types/resources/admin/eventAlerts.d.ts +60 -0
  11. package/dist/types/resources/admin/identities.d.ts +99 -0
  12. package/dist/types/resources/admin/index.d.ts +77 -0
  13. package/dist/types/resources/admin/jobs.d.ts +97 -0
  14. package/dist/types/resources/admin/myOauthApplications.d.ts +282 -0
  15. package/dist/types/resources/admin/notepads.d.ts +49 -0
  16. package/dist/types/resources/admin/oauthApplications.d.ts +309 -0
  17. package/dist/types/resources/admin/quotas.d.ts +114 -0
  18. package/dist/types/resources/admin/shortLinks.d.ts +201 -0
  19. package/dist/types/resources/admin/types.d.ts +222 -0
  20. package/dist/types/resources/admin/vocalSeparations.d.ts +98 -0
  21. package/dist/types/resources/admin.d.ts +17 -13
  22. package/dist/types/resources/auth/sessions.d.ts +6 -9
  23. package/dist/types/resources/content/analysis.d.ts +62 -0
  24. package/dist/types/resources/content/blogs.d.ts +471 -0
  25. package/dist/types/resources/content/feedbacks.d.ts +263 -0
  26. package/dist/types/resources/content/index.d.ts +52 -0
  27. package/dist/types/resources/content/intel/articles.d.ts +230 -0
  28. package/dist/types/resources/content/intel/config.d.ts +135 -0
  29. package/dist/types/resources/content/intel/index.d.ts +53 -0
  30. package/dist/types/resources/content/intel/items.d.ts +91 -0
  31. package/dist/types/resources/content/intel/reports.d.ts +108 -0
  32. package/dist/types/resources/content/intel/scripts.d.ts +152 -0
  33. package/dist/types/resources/content/intel/sources.d.ts +236 -0
  34. package/dist/types/resources/content/intel/stats.d.ts +105 -0
  35. package/dist/types/resources/content/intel/types.d.ts +86 -0
  36. package/dist/types/resources/content/jokes.d.ts +93 -0
  37. package/dist/types/resources/content/notifications.d.ts +147 -0
  38. package/dist/types/resources/content/serviceUsages.d.ts +73 -0
  39. package/dist/types/resources/content/servicesStatus.d.ts +194 -0
  40. package/dist/types/resources/content/siteConfig.d.ts +43 -0
  41. package/dist/types/resources/content/spaceInvaders.d.ts +154 -0
  42. package/dist/types/resources/content.d.ts +41 -236
  43. package/dist/types/resources/jobs.d.ts +7 -4
  44. package/dist/types/resources/library/annotations.d.ts +162 -0
  45. package/dist/types/resources/library/books.d.ts +534 -0
  46. package/dist/types/resources/library/chat.d.ts +259 -0
  47. package/dist/types/resources/library/index.d.ts +39 -0
  48. package/dist/types/resources/library/shelves.d.ts +237 -0
  49. package/dist/types/resources/library/types.d.ts +120 -0
  50. package/dist/types/resources/library.d.ts +20 -22
  51. package/dist/types/resources/movies/addons.d.ts +388 -0
  52. package/dist/types/resources/movies/collections.d.ts +352 -0
  53. package/dist/types/resources/movies/index.d.ts +29 -0
  54. package/dist/types/resources/movies/types.d.ts +50 -0
  55. package/dist/types/resources/movies/watchProgress.d.ts +277 -0
  56. package/dist/types/resources/movies.d.ts +27 -53
  57. package/dist/types/resources/music/artists.d.ts +3 -1
  58. package/dist/types/resources/music/imports.d.ts +9 -8
  59. package/dist/types/resources/music/playlists.d.ts +8 -7
  60. package/dist/types/resources/music/songs.d.ts +5 -21
  61. package/dist/types/resources/shortLinks.d.ts +5 -2
  62. package/dist/types/resources/social/groupChats.d.ts +458 -0
  63. package/dist/types/resources/social/index.d.ts +19 -0
  64. package/dist/types/resources/social/messages.d.ts +424 -0
  65. package/dist/types/resources/social/relationships.d.ts +322 -0
  66. package/dist/types/resources/social/types.d.ts +37 -0
  67. package/dist/types/resources/social.d.ts +21 -3
  68. package/dist/types/resources/storage.d.ts +24 -6
  69. package/dist/types/resources/tickets.d.ts +19 -10
  70. package/package.json +1 -1
@@ -0,0 +1,424 @@
1
+ /** Direct messages: the record, the listing and `oms.social.messages`. */
2
+ import { Resource } from "../../http";
3
+ import type { ListParams } from "../../listing";
4
+ import type { User } from "../account";
5
+ import type { FileInput, Id, NativeFile, Paginated, RequestOptions, Timestamp } from "../../types";
6
+ import type { AttachmentKind } from "./types";
7
+ /**
8
+ * Primary key of a direct message. An INTEGER: `messages` is one of the tables
9
+ * that kept an auto-increment key, so `id` and `quoted_message_id` are JSON
10
+ * numbers while `sender_id` and `receiver_id` beside them are strings.
11
+ */
12
+ export type MessageId = number;
13
+ /** `content` of a direct message is capped at 1024 characters. Longer is a `400`. */
14
+ export declare const MESSAGE_CONTENT_MAX_LENGTH = 1024;
15
+ /**
16
+ * The ceiling that actually applies to an attachment the server decides is an
17
+ * image: 20 MiB.
18
+ *
19
+ * The two caps disagree and the gap is not harmless. The server checks 25
20
+ * MiB, then hands anything whose `content_type` starts with `image/` to the
21
+ * webp recompressor, which checks 20 MiB and fails with an error that is not
22
+ * turned into a `400`. So a 22 MiB JPEG is a **500** with a Discord error
23
+ * alert behind it, where a 22 MiB zip is accepted.
24
+ *
25
+ * {@link DirectMessagesNamespace.send} refuses that band client-side when the
26
+ * size is knowable, which is the only place the mistake is still cheap.
27
+ */
28
+ export declare const MESSAGE_IMAGE_MAX_BYTES: number;
29
+ /**
30
+ * Pixel ceilings the server applies to an image attachment before any
31
+ * full-frame decode: 12000px on either side, and 50 megapixels of area. Past
32
+ * either one the answer is `400` with the dimensions in the body, and
33
+ * deliberately no error alert.
34
+ *
35
+ * Not checkable here - the SDK does not decode images - so this is
36
+ * documentation, not a guard.
37
+ */
38
+ export declare const MESSAGE_IMAGE_MAX_DIMENSION_PX = 12000;
39
+ /** @see MESSAGE_IMAGE_MAX_DIMENSION_PX */
40
+ export declare const MESSAGE_IMAGE_MAX_PIXELS = 50000000;
41
+ /** Preview of the message a message quotes. Truncated to 120 characters. */
42
+ export interface QuotedMessagePreview {
43
+ readonly id: MessageId;
44
+ readonly sender_id: Id;
45
+ readonly content: string;
46
+ readonly attachment_kind: AttachmentKind | null;
47
+ }
48
+ /**
49
+ * A direct message between two users.
50
+ *
51
+ * Every row carries BOTH ends of the conversation as fully rendered
52
+ * {@link User} objects. That is most of the bytes of a page of messages, it is
53
+ * the same two records repeated a hundred times, and there is no way to ask
54
+ * for rows without them. Budget for it on a mobile connection.
55
+ */
56
+ export interface DirectMessage {
57
+ /** Integer. */
58
+ readonly id: MessageId;
59
+ readonly created_at: Timestamp;
60
+ readonly updated_at: Timestamp;
61
+ /** Never null and never empty. */
62
+ readonly content: string;
63
+ readonly sender_id: Id;
64
+ readonly receiver_id: Id;
65
+ /**
66
+ * DO NOT READ THIS AS "THE OTHER PERSON SAW IT".
67
+ *
68
+ * It is set by {@link DirectMessagesNamespace.list} on every row the listing
69
+ * matched, in both directions, so it means "somebody who can see this
70
+ * message listed it" and nothing more precise. On a message the CALLER
71
+ * sent, it was almost certainly stamped by the caller's own listing rather
72
+ * than by the recipient's - opening a thread marks your own outgoing
73
+ * messages read. A "seen" tick built on this field lies.
74
+ *
75
+ * On a message the caller RECEIVED it is meaningful, in the weak sense that
76
+ * their own client fetched it at that time. It is what
77
+ * {@link DirectMessagesNamespace.conversedUsers} counts, and the only
78
+ * unread signal the API has.
79
+ */
80
+ readonly read_at: Timestamp | null;
81
+ readonly attachment_kind: AttachmentKind | null;
82
+ readonly quoted_message_id: MessageId | null;
83
+ /** Null until the sender edits it. */
84
+ readonly edited_at: Timestamp | null;
85
+ readonly has_image: boolean;
86
+ readonly has_attachment: boolean;
87
+ readonly attachment_filename: string | null;
88
+ readonly attachment_byte_size: number | null;
89
+ readonly quoted_preview: QuotedMessagePreview | null;
90
+ /** The full sender record, nested. */
91
+ readonly sender: User;
92
+ /** The full receiver record, nested. */
93
+ readonly receiver: User;
94
+ }
95
+ /** One row of {@link DirectMessagesNamespace.conversedUsers}. */
96
+ export interface ConversedUser extends User {
97
+ readonly last_message: {
98
+ readonly id: MessageId;
99
+ readonly preview: string;
100
+ readonly from_me: boolean;
101
+ readonly created_at: Timestamp;
102
+ } | null;
103
+ readonly unread_count: number;
104
+ }
105
+ /** Filter columns of `GET /messages`. */
106
+ export declare const MESSAGE_FILTER_COLUMNS: readonly ["id", "created_at", "updated_at", "content", "sender_id", "receiver_id", "read_at"];
107
+ /** `extra_options` keys of `GET /messages`. */
108
+ export declare const MESSAGE_EXTRA_OPTION_KEYS: readonly ["user_id", "other_user_id"];
109
+ /** `extra_options` of `GET /messages`. Each matches rows where the user is either end. */
110
+ export interface MessageExtraOptions {
111
+ readonly user_id?: Id;
112
+ readonly other_user_id?: Id;
113
+ }
114
+ /** Filters accepted by {@link DirectMessagesNamespace.list}. */
115
+ export interface ListMessagesParams extends ListParams<(typeof MESSAGE_FILTER_COLUMNS)[number], MessageExtraOptions> {
116
+ /**
117
+ * The other end of the conversation. Sent as
118
+ * `extra_options[other_user_id]`, which the server expands to
119
+ * `sender_id = ? OR receiver_id = ?` INSIDE the caller's own scope, so it
120
+ * yields exactly the thread with that person.
121
+ *
122
+ * Pass this on every call you can. It is the only thing that keeps
123
+ * {@link DirectMessagesNamespace.list}'s read-marking side effect from
124
+ * covering your whole mailbox - see the method's docs.
125
+ */
126
+ readonly withUser?: Id;
127
+ /**
128
+ * `extra_options[user_id]`: rows where this user is EITHER end.
129
+ *
130
+ * Redundant for a normal caller - the listing scope is already the caller's
131
+ * own messages, so passing your own id narrows nothing - and it cannot widen
132
+ * the scope to somebody else's mailbox either. It is exposed only so a
133
+ * caller porting existing code can keep the wire identical.
134
+ */
135
+ readonly userId?: Id;
136
+ /** `search[content]`: partial, case- and accent-insensitive. */
137
+ readonly contentContains?: string;
138
+ /** `exact_search[sender_id]`. */
139
+ readonly senderId?: Id;
140
+ /** `exact_search[receiver_id]`. */
141
+ readonly receiverId?: Id;
142
+ /**
143
+ * `exact_search[read_at]` set to the null sentinel, i.e. `read_at IS NULL`.
144
+ *
145
+ * Reading unread messages is what MARKS them read, so this filter is
146
+ * self-consuming: the same call that returns them empties the set it
147
+ * matched. Use {@link DirectMessagesNamespace.conversedUsers} for a badge
148
+ * count that survives being looked at.
149
+ */
150
+ readonly unreadOnly?: boolean;
151
+ }
152
+ /** Body of {@link DirectMessagesNamespace.send}. */
153
+ export interface SendMessageInput {
154
+ /** Recipient. A user id string, never a handle. */
155
+ readonly receiverId: Id;
156
+ /**
157
+ * Up to {@link MESSAGE_CONTENT_MAX_LENGTH} characters.
158
+ *
159
+ * Required UNLESS `attachment` is present: an attachment-only message has
160
+ * the literal string `"No Content"` substituted for its missing caption. So
161
+ * an attachment sent with no caption comes back with
162
+ * `content: "No Content"`, in English, unlocalised, and every client has to
163
+ * special-case that string when rendering. Send a caption if you have one.
164
+ */
165
+ readonly content?: string;
166
+ /**
167
+ * One file, up to {@link MESSAGE_ATTACHMENT_MAX_BYTES} - or
168
+ * {@link MESSAGE_IMAGE_MAX_BYTES} if the server decides it is an image.
169
+ *
170
+ * A React Native pick (`{ uri, name, type }`) may be passed straight through;
171
+ * the transport appends it verbatim to RN's `FormData` and refuses loudly on
172
+ * any other runtime, where it would silently upload as `"[object Object]"`.
173
+ *
174
+ * An `image/*` attachment is NOT stored as sent: the server re-encodes it to
175
+ * webp (quality 75, or 50 past 5 MiB) and renames it
176
+ * `message_image_<timestamp>.webp`, so `attachment_filename` on the record
177
+ * will not be the name you gave. Everything else is stored byte for byte.
178
+ */
179
+ readonly attachment?: FileInput | NativeFile;
180
+ /**
181
+ * Id of a message to quote. Any message id the server can find - there is
182
+ * no check that the quoted row is in this conversation, or even visible to
183
+ * either party. `quoted_preview` on the answer is rendered from it
184
+ * unconditionally, so quoting a stranger's message id leaks 120 characters
185
+ * of it. Only ever pass an id you took out of this same thread.
186
+ */
187
+ readonly quotedMessageId?: MessageId;
188
+ }
189
+ /**
190
+ * One-to-one messages, reachable as `oms.social.messages`.
191
+ *
192
+ * ## `list()` IS NOT A READ
193
+ *
194
+ * {@link DirectMessagesNamespace.list} marks every matched row read before it
195
+ * serialises anything. Read it in full before you call it from a poller, a
196
+ * prefetch, or a retry loop.
197
+ */
198
+ export declare class DirectMessagesNamespace extends Resource {
199
+ /**
200
+ * `GET /messages` - the caller's messages, sent and received, as one listing.
201
+ *
202
+ * ## This GET writes. Three ways it will surprise you.
203
+ *
204
+ * The server marks the matched rows read before rendering, and it does so
205
+ * on the UNBOUNDED scope: every filter applies, and the page modifier
206
+ * deliberately does not, because marking only the first page would leave
207
+ * older messages showing unread. The consequences:
208
+ *
209
+ * 1. **An unfiltered `list()` marks your ENTIRE mailbox read**, every
210
+ * conversation, in one `UPDATE`. There is no read-only variant. Always
211
+ * pass {@link ListMessagesParams.withUser}.
212
+ * 2. **It also stamps `read_at` on messages YOU sent.** The listing covers
213
+ * sent and received alike, and the read-marking does not know the
214
+ * difference. So opening a thread sets `read_at` on your own outgoing
215
+ * messages, and any UI that renders "seen" from `read_at` on the sender's
216
+ * side is reading your own visit back to you as the recipient's. Treat
217
+ * `read_at` on a message whose `sender_id` is yours as meaningless.
218
+ * 3. **`updated_at` does NOT move.** The index's `ETag` is computed from the
219
+ * relation's count and its maximum `updated_at`, so a conditional GET can
220
+ * answer `304` while `read_at` underneath has changed. The SDK never
221
+ * sends `If-None-Match`, so this only bites a host that added its own
222
+ * caching layer.
223
+ *
224
+ * Retrying is safe in the sense that the second `UPDATE` writes the same
225
+ * rows again; it is not safe in the sense that the damage is already done on
226
+ * attempt one.
227
+ *
228
+ * ## Ordering and paging
229
+ *
230
+ * The server declares no default order. Offset pagination over an unordered
231
+ * query in Postgres may repeat a row on one page and drop it from the next,
232
+ * so the SDK always sends one: `created_at:desc` unless you pass `order`.
233
+ * Newest first is what a chat view wants, since it opens at the bottom.
234
+ *
235
+ * With no page modifier at all the server would force `1:500`; the SDK sends
236
+ * an explicit page so {@link Paginated.pageSize} matches what the rows were
237
+ * counted against.
238
+ *
239
+ * ## Filters that do not exist
240
+ *
241
+ * The filter columns are `id, created_at, updated_at, content, sender_id,
242
+ * receiver_id, read_at`, and unknown keys FAIL CLOSED with `400 "Unknown
243
+ * search filter: ..."` rather than being dropped. So there is no filter for
244
+ * `attachment_kind`, none for `edited_at`, and no way to ask for "messages
245
+ * since X" other than `search[created_at]`, which is a partial string match
246
+ * on the rendered timestamp and is not a range query. Fetch and filter here.
247
+ *
248
+ * Costs one request against the general authenticated ceiling (600/min).
249
+ */
250
+ list(params?: ListMessagesParams, options?: RequestOptions): Promise<Paginated<DirectMessage>>;
251
+ /**
252
+ * One conversation, newest first. `list()` with
253
+ * {@link ListMessagesParams.withUser} already filled in.
254
+ *
255
+ * This is the call a chat screen should make, and the reason it exists as its
256
+ * own method is the read-marking described on {@link list}: scoping the
257
+ * listing is what scopes the `UPDATE`. Opening a thread marks that thread
258
+ * read, which is what a user expects. Calling `list()` bare does not.
259
+ */
260
+ conversation(userId: Id, params?: Omit<ListMessagesParams, "withUser">, options?: RequestOptions): Promise<Paginated<DirectMessage>>;
261
+ /**
262
+ * `GET /messages/conversed_users` - one row per person the caller has ever
263
+ * exchanged a message with, newest conversation first, each carrying the
264
+ * latest message either way and the caller's unread count from that sender.
265
+ *
266
+ * This is the inbox list, and it is the ONLY endpoint here that reports
267
+ * unread counts without destroying them: it does not mark anything read.
268
+ * Poll this one, not {@link list}.
269
+ *
270
+ * Not paginated and not filterable - it is a bespoke action, not a listing,
271
+ * so it carries no `ETag` and ignores every modifier. The whole set comes
272
+ * back on every call, and the cost is bounded by how many people you have
273
+ * talked to rather than by how many messages there are: one `DISTINCT ON`
274
+ * for the latest message per counterpart, one grouped `COUNT` for the unread
275
+ * numbers, and only that bounded set is hydrated.
276
+ *
277
+ * `last_message.preview` is 120 characters of content, or a bracketed
278
+ * placeholder (`"[image]"`, `"[audio]"`, `"[video]"`, `"[file]"`) when the
279
+ * newest message is an attachment. It is always a string, never `null`.
280
+ *
281
+ * A user you have messaged appears here even if one of you has since blocked
282
+ * the other; the rows are built from message history, not from
283
+ * {@link Relationship}. Cross-reference
284
+ * {@link RelationshipsNamespace.list} if the UI needs to hide those.
285
+ */
286
+ conversedUsers(options?: RequestOptions): Promise<ConversedUser[]>;
287
+ /**
288
+ * `GET /messages/:id` - one message, by id.
289
+ *
290
+ * Scoped to the caller's own messages, so a message between two other people
291
+ * is `404 "Resource not found"` rather than `403`. Does NOT mark anything
292
+ * read, which makes it the safe way to re-read a single row.
293
+ *
294
+ * Returns exactly the same fields as a row from {@link list}.
295
+ */
296
+ get(id: MessageId, options?: RequestOptions): Promise<DirectMessage>;
297
+ /**
298
+ * `POST /messages` - sends one message. `201` with the created record.
299
+ *
300
+ * The sender is always the authenticated user; there is no way to say
301
+ * otherwise and `sender_id` in the body is ignored.
302
+ *
303
+ * ## What "the recipient blocked me" looks like
304
+ *
305
+ * A `401 "You are not authorized to create this resource"` is how a block is
306
+ * announced - after the request, never before it, because the block that
307
+ * stops you is invisible to you: rows where you are the `accepter` of a
308
+ * `block` are hidden from you. You cannot look up whether you are blocked;
309
+ * you can only be refused.
310
+ *
311
+ * The test is one-directional. If YOU blocked THEM, nothing here stops you
312
+ * from messaging them, and the message is delivered.
313
+ *
314
+ * ## Transport
315
+ *
316
+ * Sent as JSON when there is no attachment and as `multipart/form-data` when
317
+ * there is - the same route accepts both, and multipart is what makes a file
318
+ * possible at all. In multipart the numeric `quoted_message_id` goes out as
319
+ * text and the server casts it back; that is normal and not a precision risk
320
+ * at these magnitudes.
321
+ *
322
+ * The field is named `attachment`. A legacy `image` field is still accepted
323
+ * and funnelled to the same blob, but nothing new should send it.
324
+ *
325
+ * ## What is refused here rather than by the server
326
+ *
327
+ * An image between {@link MESSAGE_IMAGE_MAX_BYTES} and
328
+ * {@link MESSAGE_ATTACHMENT_MAX_BYTES} passes the general cap and then dies
329
+ * inside the webp recompressor, which is a **500** with an error alert behind
330
+ * it rather than a `400`. This method throws before sending when the size is
331
+ * knowable. It stays silent when it is not - a `ReadableStream` with no
332
+ * declared `size`, or a picker that reported none - because buffering a file
333
+ * just to measure it would be worse than the server's answer.
334
+ *
335
+ * NOT retried: this is a `POST`, and a replay after a lost response sends the
336
+ * message twice.
337
+ *
338
+ * @throws {OmsError} `invalid_request` when neither content nor an
339
+ * attachment is present, when content is over the length cap, or when a
340
+ * knowably-oversized image would 500.
341
+ * @throws {OmsApiError} `401` when the recipient has blocked the caller,
342
+ * `400` when the recipient does not exist or the content is empty.
343
+ */
344
+ send(input: SendMessageInput, options?: RequestOptions): Promise<DirectMessage>;
345
+ /**
346
+ * `PATCH /messages/:id` - rewrites the content of a message you sent.
347
+ *
348
+ * `content` is the only writable field. An attachment cannot be added,
349
+ * replaced or removed after the fact, and `receiver_id` is fixed.
350
+ *
351
+ * Allowed only while you are the sender AND the message is younger than
352
+ * {@link MESSAGE_EDIT_WINDOW_MS}. Past the window the answer is `401`, not
353
+ * `403` and not `422`. Use {@link canEditMessage} to grey the button out,
354
+ * and still handle the `401`: the window is measured on the server's clock.
355
+ *
356
+ * A successful edit stamps `edited_at`, which every client renders as an
357
+ * "edited" marker. It is stamped only when the content actually changes: a
358
+ * no-op PATCH with an identical string does NOT mark it edited, but any
359
+ * different string does, including whitespace.
360
+ *
361
+ * There is no cable broadcast for an edit. The other side sees the new text
362
+ * whenever it next fetches, and never learns that it changed.
363
+ */
364
+ edit(id: MessageId, content: string, options?: RequestOptions): Promise<DirectMessage>;
365
+ /**
366
+ * `DELETE /messages/:id` - `204`, empty body.
367
+ *
368
+ * The sender may delete at any time; there is no edit window on destroy and
369
+ * the receiver cannot delete a message they were sent. The row and its blob
370
+ * go for both parties at once, so this is "unsend", not "hide from my copy".
371
+ *
372
+ * A message that another message quotes can still be deleted. The quoting
373
+ * row keeps its `quoted_preview` - the snapshot was serialised at render
374
+ * time from the association, so once the quoted row is gone the preview
375
+ * becomes `null` on the next fetch and the quote silently empties.
376
+ *
377
+ * Not retried, like every write. A `DELETE` replayed after a torn connection
378
+ * would answer `404` for a row it deleted perfectly well.
379
+ */
380
+ delete(id: MessageId, options?: RequestOptions): Promise<void>;
381
+ /**
382
+ * `GET /messages/:id/attachment` - the bytes of the attached file.
383
+ *
384
+ * The route answers `302` to a presigned object-store URL, and `fetch`
385
+ * follows that redirect on its own. Which means this method works everywhere
386
+ * EXCEPT a browser in cookie mode: the request carries credentials, the
387
+ * redirected request arrives at the store with `Origin: null`, and the store
388
+ * answers a wildcard `Access-Control-Allow-Origin`, which CORS forbids for a
389
+ * credentialed request. There, use {@link attachmentUrl} and let an `<img>`,
390
+ * a `<video>` or an `<a download>` follow the redirect with no CORS check.
391
+ *
392
+ * Filename, byte size and kind are already on the {@link DirectMessage},
393
+ * which is why this hands back bare bytes.
394
+ *
395
+ * @throws {OmsError} `unsupported` when the redirect was blocked by CORS.
396
+ * @throws {OmsApiError} `404 "Message not found"` when the message is not
397
+ * yours, `404 "No attachment"` when it carries none.
398
+ */
399
+ attachment(id: MessageId, options?: RequestOptions): Promise<Blob>;
400
+ /**
401
+ * Absolute URL for a message attachment, for an `<img>`, an `<a download>` or
402
+ * a new tab. In a browser in cookie mode this is the only way to reach the
403
+ * bytes; see {@link attachment}.
404
+ *
405
+ * Asynchronous because the credential has to be resolved first, and resolving
406
+ * it may involve a token refresh.
407
+ *
408
+ * - **cookie mode**: no credential in the URL; the browser attaches the
409
+ * host-only `oms_session` cookie itself. Do not put `crossorigin` on the
410
+ * element - that turns a no-cors load into a CORS one and re-creates the
411
+ * failure this method exists to avoid.
412
+ * - **token mode**: the token is appended as `?token=`, which is what a
413
+ * native client needs because an `<img>` cannot carry a header.
414
+ *
415
+ * THE TOKEN-MODE URL CONTAINS A LIVE CREDENTIAL. Build it at the moment of
416
+ * use, never store it, never log it, and never hand it to anything outside
417
+ * your own page - fetch the bytes with {@link attachment} and pass a `blob:`
418
+ * URL instead.
419
+ *
420
+ * There is a legacy twin at `/messages/:id/image`. It resolves to the same
421
+ * blob for image messages and is not worth calling from new code.
422
+ */
423
+ attachmentUrl(id: MessageId): Promise<string>;
424
+ }