@omelhorsite/sdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/index.js +4939 -552
  2. package/dist/types/client.d.ts +60 -3
  3. package/dist/types/http.d.ts +444 -19
  4. package/dist/types/index.d.ts +4 -1
  5. package/dist/types/resources/account.d.ts +66 -3
  6. package/dist/types/resources/admin.d.ts +1837 -0
  7. package/dist/types/resources/auth/index.d.ts +39 -0
  8. package/dist/types/resources/auth/passkeys.d.ts +652 -0
  9. package/dist/types/resources/auth/sessions.d.ts +847 -0
  10. package/dist/types/resources/chests.d.ts +54 -3
  11. package/dist/types/resources/content.d.ts +2970 -0
  12. package/dist/types/resources/dynamicQrs.d.ts +39 -3
  13. package/dist/types/resources/forms.d.ts +176 -35
  14. package/dist/types/resources/index.d.ts +19 -8
  15. package/dist/types/resources/ipLookup.d.ts +20 -4
  16. package/dist/types/resources/jobs.d.ts +62 -21
  17. package/dist/types/resources/library.d.ts +1435 -0
  18. package/dist/types/resources/linkTrees.d.ts +142 -30
  19. package/dist/types/resources/media.d.ts +351 -0
  20. package/dist/types/resources/movies.d.ts +1186 -0
  21. package/dist/types/resources/music/artists.d.ts +1066 -0
  22. package/dist/types/resources/music/imports.d.ts +940 -0
  23. package/dist/types/resources/music/index.d.ts +61 -0
  24. package/dist/types/resources/music/playlists.d.ts +1026 -0
  25. package/dist/types/resources/music/social.d.ts +1132 -0
  26. package/dist/types/resources/music/songs.d.ts +1183 -0
  27. package/dist/types/resources/notepads.d.ts +4 -1
  28. package/dist/types/resources/quotas.d.ts +7 -1
  29. package/dist/types/resources/realtime.d.ts +855 -0
  30. package/dist/types/resources/shortLinks.d.ts +45 -4
  31. package/dist/types/resources/social.d.ts +1330 -0
  32. package/dist/types/resources/storage/upload.d.ts +158 -11
  33. package/dist/types/resources/storage.d.ts +88 -22
  34. package/dist/types/resources/tickets.d.ts +82 -3
  35. package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
  36. package/dist/types/resources/tools/captions.d.ts +448 -21
  37. package/dist/types/resources/tools/downloader.d.ts +21 -0
  38. package/dist/types/resources/tools/index.d.ts +57 -15
  39. package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
  40. package/dist/types/resources/tools/transcription.d.ts +35 -13
  41. package/dist/types/resources/tools/upscale.d.ts +23 -3
  42. package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
  43. package/dist/types/types.d.ts +249 -17
  44. package/package.json +2 -1
@@ -10,26 +10,56 @@
10
10
  */
11
11
  import { Resource } from "../http";
12
12
  import type { BaseRecord, FileInput, Id, RequestOptions, Timestamp } from "../types";
13
- /** Longest a slug may be. The shortest is 2. */
13
+ /**
14
+ * The reserved short-link namespace a link tree is served under.
15
+ * `ShortLink::LINK_TREE_NAMESPACE`.
16
+ *
17
+ * Note it is `"t"` and not `"lt"`. The model's own comment says `"lt"`; the
18
+ * constant says `"t"`, and the constant is what the rows carry.
19
+ */
20
+ export declare const LINK_TREE_NAMESPACE = "t";
21
+ /** Public prefix a link tree resolves under. */
22
+ export declare const LINK_TREE_BASE_URL = "https://omelhor.site/t";
23
+ /** Longest a slug may be. */
14
24
  export declare const LINK_TREE_SLUG_MAX_LENGTH = 63;
25
+ /**
26
+ * Shortest a slug may actually be, which is **3** and not the 2 that
27
+ * `LinkTree::SLUG_MIN` claims.
28
+ *
29
+ * Two validations run, and the tighter one wins. The length check allows 2,
30
+ * but `SLUG_FORMAT` is `/\A[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])?\z/`: either the
31
+ * optional group is absent, which matches ONE character and then fails the
32
+ * length check, or it is present and contributes at least two more. There is
33
+ * no way to spell a valid two-character slug, so a caller that trusts
34
+ * `SLUG_MIN` gets a 400 saying the slug "contains invalid characters".
35
+ */
36
+ export declare const LINK_TREE_SLUG_MIN_LENGTH = 3;
15
37
  /** Most items one tree may carry. */
16
38
  export declare const LINK_TREE_MAX_ITEMS = 30;
17
39
  /** Ceiling on the attached CV, which must also be a PDF. */
18
40
  export declare const LINK_TREE_CV_MAX_BYTES: number;
19
41
  /**
20
- * One entry on a link tree.
42
+ * One entry on a link tree, AS READ back.
21
43
  *
22
- * `url` must carry an `http`, `https`, `mailto`, `tel` or `sms` scheme; a bare
23
- * domain is rejected by the model, not silently fixed.
44
+ * `id` is not optional here even though it is optional when writing:
45
+ * `LinkTrees::InputSanitizer#items` mints `SecureRandom.uuid` for any entry
46
+ * that arrives without one, so a stored entry always has one - and it has to,
47
+ * because it is the key `clicks_by_item` is filed under.
48
+ *
49
+ * `label` and `url` are likewise always present, because an entry missing
50
+ * either is DROPPED from the list rather than stored blank. The three
51
+ * remaining keys are genuinely absent when unset: the sanitiser only writes
52
+ * `icon` and `description` when non-blank, and `icon_image` only when it is a
53
+ * `data:image/` URI under the cap.
54
+ *
55
+ * Write with {@link LinkTreeItemInput}.
24
56
  */
25
57
  export interface LinkTreeItem {
26
- /** Stable within the tree, and the key click tracking is filed under. The
27
- * server mints a UUID when you leave it out, so read the tree back before
28
- * calling {@link LinkTreesNamespace.trackClick}. */
29
- readonly id?: string;
30
- /** Up to 80 characters. */
58
+ /** Stable within the tree, and the key `clicks_by_item` is filed under. */
59
+ readonly id: string;
60
+ /** Up to 80 characters, and never blank. */
31
61
  readonly label: string;
32
- /** Up to 1024 characters, with a scheme. */
62
+ /** Up to 1024 characters, and never blank. */
33
63
  readonly url: string;
34
64
  /** Icon slug the renderer resolves. Up to 40 characters. */
35
65
  readonly icon?: string;
@@ -38,6 +68,34 @@ export interface LinkTreeItem {
38
68
  /** `data:image/...` under 80 KB, for a custom icon. */
39
69
  readonly icon_image?: string;
40
70
  }
71
+ /**
72
+ * One entry as WRITTEN.
73
+ *
74
+ * `url` must carry an `http`, `https`, `mailto`, `tel` or `sms` scheme; a bare
75
+ * domain is rejected by the model, not silently fixed. An entry whose `label`
76
+ * or `url` is blank is dropped by the sanitiser BEFORE validation, so it
77
+ * disappears without an error rather than failing the write.
78
+ *
79
+ * A {@link LinkTreeItem} read off a tree is assignable here, so the
80
+ * read-edit-write round trip needs no mapping - and keeping each item's `id`
81
+ * through that round trip is what keeps `clicks_by_item` lined up with
82
+ * anything.
83
+ */
84
+ export interface LinkTreeItemInput {
85
+ /** Omit and the server mints a UUID, which you then have to read back before
86
+ * you can call {@link LinkTreesNamespace.trackClick} for this entry. */
87
+ readonly id?: string;
88
+ readonly label: string;
89
+ readonly url: string;
90
+ readonly icon?: string;
91
+ readonly description?: string;
92
+ /**
93
+ * `data:image/...` under 80 KB. `null` and any other non-conforming value
94
+ * are DROPPED by the sanitiser rather than rejected, so sending `null` here
95
+ * clears the icon by omission and answers 200 either way.
96
+ */
97
+ readonly icon_image?: string | null;
98
+ }
41
99
  /**
42
100
  * Styling of a link tree. Colours must be `#RRGGBB` and images must be
43
101
  * `data:image/` URIs; anything else in here is dropped in silence on write.
@@ -56,38 +114,60 @@ export interface LinkTreeTheme {
56
114
  /** `data:image/...` under 1 MB, or `null` to clear. */
57
115
  readonly banner_image?: string | null;
58
116
  }
59
- /** A link-in-bio page, owner view. */
117
+ /**
118
+ * A link-in-bio page, owner view.
119
+ *
120
+ * Every key is present on every owner-facing response: index, show, create,
121
+ * update, `uploadCv` and `removeCv` all render the blueprint's DEFAULT view,
122
+ * never `:extended`, so there is no richer variant and nothing here is
123
+ * conditional. Four of the values are nullable.
124
+ */
60
125
  export interface LinkTree extends BaseRecord {
61
126
  readonly user_id: Id;
62
127
  /** Public path segment. Lowercase letters, digits and dashes. */
63
128
  readonly slug: string;
129
+ /** Never blank: the model validates presence. Up to 80 characters. */
64
130
  readonly title: string;
65
- readonly bio?: string | null;
66
- /** The avatar, inline as a `data:image/` URI rather than a URL. */
67
- readonly avatar_data_url?: string | null;
131
+ /** Up to 280 characters, or `null`. Never `undefined`. */
132
+ readonly bio: string | null;
133
+ /** The avatar, inline as a `data:image/` URI rather than a URL, or `null`. */
134
+ readonly avatar_data_url: string | null;
135
+ /** Never `null`: the blueprint substitutes `[]` for an unset list. */
68
136
  readonly items: LinkTreeItem[];
137
+ /** Never `null`: the blueprint substitutes `{}` for an unset bag. */
69
138
  readonly theme: LinkTreeTheme;
70
- /** Click counters keyed by {@link LinkTreeItem.id}. */
139
+ /**
140
+ * Click counters keyed by {@link LinkTreeItem.id}. Never `null` - `{}` for a
141
+ * tree nobody has clicked - and it can hold ids of items that have since
142
+ * been deleted, because nothing prunes it.
143
+ */
71
144
  readonly clicks_by_item: Record<string, number>;
72
145
  /** Absolute URL that downloads the CV, or `null` when none is attached. */
73
146
  readonly cv_url: string | null;
74
147
  readonly cv_filename: string | null;
75
- /** The shareable short URL. */
148
+ /** The shareable short URL: `{@link LINK_TREE_BASE_URL}/{slug}`. */
76
149
  readonly public_url: string;
77
150
  /** Endpoint of the paired short link. Always equal to `slug`. */
78
151
  readonly short_link_endpoint: string;
79
- readonly short_link_namespace: string;
152
+ /** Always {@link LINK_TREE_NAMESPACE}; the blueprint renders the constant. */
153
+ readonly short_link_namespace: typeof LINK_TREE_NAMESPACE;
80
154
  }
81
155
  /**
82
- * The visitor's view: the same page with the owner-only fields removed. Note
83
- * that it carries no timestamps, so it is NOT a {@link LinkTree}.
156
+ * The visitor's view: the same page with the owner-only fields removed.
157
+ *
158
+ * The `:public` view excludes exactly seven keys - `user_id`,
159
+ * `clicks_by_item`, `public_url`, `short_link_endpoint`,
160
+ * `short_link_namespace`, `created_at` and `updated_at` - and INHERITS
161
+ * everything else from the default view, which is why `id`, `cv_url` and
162
+ * `cv_filename` are still here. It carries no timestamps, so it is NOT a
163
+ * {@link LinkTree} and cannot be passed where one is expected.
84
164
  */
85
165
  export interface PublicLinkTree {
86
166
  readonly id: Id;
87
167
  readonly slug: string;
88
168
  readonly title: string;
89
- readonly bio?: string | null;
90
- readonly avatar_data_url?: string | null;
169
+ readonly bio: string | null;
170
+ readonly avatar_data_url: string | null;
91
171
  readonly items: LinkTreeItem[];
92
172
  readonly theme: LinkTreeTheme;
93
173
  readonly cv_url: string | null;
@@ -96,18 +176,27 @@ export interface PublicLinkTree {
96
176
  /** Arguments for creating a link tree. */
97
177
  export interface CreateLinkTreeInput {
98
178
  /**
99
- * 2 to {@link LINK_TREE_SLUG_MAX_LENGTH} characters of `[a-z0-9-]`, starting
100
- * and ending alphanumeric, and never one of `new edit admin api root login
101
- * logout signup signin manage`.
179
+ * {@link LINK_TREE_SLUG_MIN_LENGTH} to {@link LINK_TREE_SLUG_MAX_LENGTH}
180
+ * characters of `[a-z0-9-]`, starting and ending alphanumeric, and never one
181
+ * of `new edit admin api root login logout signup signin manage`.
182
+ *
183
+ * Lowercased and trimmed server-side before any check, so casing is not a
184
+ * reason to be refused.
102
185
  */
103
186
  readonly slug: string;
104
187
  /** Required: the model refuses a blank title. Up to 80 characters. */
105
188
  readonly title: string;
106
189
  /** Up to 280 characters. */
107
190
  readonly bio?: string;
108
- /** `data:image/...` under 500 KB. Build one with `dataUrlFromFile`. */
191
+ /**
192
+ * `data:image/...` under 500 KB. Anything else - a plain URL, an oversized
193
+ * image - is turned into `null` by the sanitiser before validation, so a bad
194
+ * avatar creates the tree WITHOUT one rather than failing. Read
195
+ * `avatar_data_url` back off the answer.
196
+ */
109
197
  readonly avatarDataUrl?: string;
110
- readonly items?: LinkTreeItem[];
198
+ /** At most {@link LINK_TREE_MAX_ITEMS}; more is a 400. */
199
+ readonly items?: LinkTreeItemInput[];
111
200
  readonly theme?: LinkTreeTheme;
112
201
  }
113
202
  /**
@@ -123,9 +212,16 @@ export interface UpdateLinkTreeInput {
123
212
  /** Renames the paired short link; the old public URL stops resolving. */
124
213
  readonly slug?: string;
125
214
  readonly title?: string;
126
- readonly bio?: string;
215
+ /**
216
+ * Accepts `null`, but note what it does: the controller assigns
217
+ * `params[:bio].to_s.strip`, so `null` stores the empty STRING and not
218
+ * `null`. There is no way through this endpoint to put the column back to
219
+ * `null` once it holds a value; `""` is as empty as it gets.
220
+ */
221
+ readonly bio?: string | null;
222
+ /** `null` or `""` clears the avatar. Anything malformed also clears it. */
127
223
  readonly avatarDataUrl?: string | null;
128
- readonly items?: LinkTreeItem[];
224
+ readonly items?: LinkTreeItemInput[];
129
225
  readonly theme?: LinkTreeTheme;
130
226
  }
131
227
  /** One day of the click histogram. Always 30 entries, oldest first. */
@@ -171,8 +267,14 @@ export interface LinkTreeSlugAvailability {
171
267
  readonly slug: string;
172
268
  readonly valid: boolean;
173
269
  readonly available: boolean;
174
- /** `"invalid"` for the format, `"reserved"` for the blocklist. */
175
- readonly reason?: string;
270
+ /**
271
+ * Only when `valid` is `false`. `"invalid"` for the format or the length,
272
+ * `"reserved"` for the blocklist - link trees distinguish the two, unlike
273
+ * the otherwise identical `FormEndpointAvailability`, which only ever says
274
+ * `"invalid"`.
275
+ */
276
+ readonly reason?: "invalid" | "reserved";
277
+ /** Only when `valid` is `true` and `available` is `false`. */
176
278
  readonly suggestions?: string[];
177
279
  }
178
280
  /** The `linkTrees` namespace, reachable as `oms.linkTrees`. */
@@ -246,4 +348,14 @@ export declare class LinkTreesNamespace extends Resource {
246
348
  * which slugs exist.
247
349
  */
248
350
  trackClick(slug: string, itemId: string, options?: RequestOptions): Promise<void>;
351
+ /**
352
+ * The public URL a slug is served from. Pure string building, no request, and
353
+ * the same string the server puts in {@link LinkTree.public_url}.
354
+ *
355
+ * Prefer `tree.public_url` when you are holding an owner-view record. Reach
356
+ * for this when all you have is a slug - after {@link slugAvailability}, or
357
+ * from a {@link PublicLinkTree}, whose `:public` view deliberately drops
358
+ * `public_url` along with the rest of the pairing metadata.
359
+ */
360
+ publicUrl(slug: string): string;
249
361
  }
@@ -0,0 +1,351 @@
1
+ /**
2
+ * The `media` namespace: the canonical bytes of the music library.
3
+ *
4
+ * A song, an artist and a playlist never carry bytes or URLs inline. They carry
5
+ * MEDIA IDS - `audio_media_id`, `compressed_artwork_media_id`,
6
+ * `image_media_id`, `vocals_media_id` and the rest - and this namespace is the
7
+ * one place those ids turn into something playable.
8
+ *
9
+ * ## Two routes, and which one is the real one
10
+ *
11
+ * - `GET /media/:id/data` and `GET /media/:id/data_url` are the CANONICAL
12
+ * routes. `MediaController` serves them, and every new client should use
13
+ * them.
14
+ * - `GET /fs_nodes/:id/data` and `GET /fs_nodes/:id/data_url` are a TEMPORARY
15
+ * ALIAS, in the backend's own words: "the old `/fs_nodes/:id/data{,_url}`
16
+ * routes keep a temporary numeric-id alias for the web frontend".
17
+ * `FsNodesController` branches on `params[:id] =~ /\A\d+\z/` and hands a
18
+ * numeric id to the same `MusicMediaServing` concern; a storage UUID keeps
19
+ * the old filesystem behaviour. The alias exists because the web frontend
20
+ * still builds those URLs, and it is scheduled to go together with the
21
+ * `*_fs_node_id` blueprint twins.
22
+ *
23
+ * {@link MediaNamespace.aliasUrl} and {@link MediaNamespace.aliasDataUrl} are
24
+ * here so a port of the old web code type-checks, and for the one case where
25
+ * the alias is genuinely more capable (see the OAuth note below). Reach for
26
+ * {@link MediaNamespace.url} and {@link MediaNamespace.dataUrl} in new code.
27
+ *
28
+ * ## A media id is not a storage node id
29
+ *
30
+ * It is an `active_storage_attachments` primary key, which
31
+ * `ApplicationBlueprint.media_id_fields` serialises with `.to_s` - so it is a
32
+ * STRING whose characters happen to all be digits (`"48211"`). Storage node
33
+ * ids are uuids. The two id spaces are not interchangeable, and the only
34
+ * reason a media id works on the `fs_nodes` path at all is the numeric branch
35
+ * described above. {@link isMediaId} exists to keep that straight.
36
+ *
37
+ * ## 404 NEVER 401, and why that will empty somebody's library
38
+ *
39
+ * `resolve_music_attachment` returns `nil` - and the controller answers `404
40
+ * "Not found"` - for every one of these, deliberately indistinguishable:
41
+ *
42
+ * - the id does not exist;
43
+ * - it exists but belongs to a book, a tool output, another user;
44
+ * - the caller sent no credential at all;
45
+ * - the caller sent a credential that no longer resolves to a live session.
46
+ *
47
+ * The routes are declared `allow_unauthenticated_access`, so authentication
48
+ * never gets a chance to answer `401`. That is correct for the server:
49
+ * existence must not leak. It is a TRAP for the client.
50
+ *
51
+ * A client that reads `404` as "this file is gone" will, the moment a session
52
+ * expires or a token rotates, quietly render an entire library of broken
53
+ * artwork and unplayable tracks - and it will look like data loss, not like a
54
+ * sign-in problem, because nothing anywhere returned `401`. **Never conclude
55
+ * "missing" from a media 404 alone.** Confirm the session first with a route
56
+ * that does distinguish the two (`oms.account.me()` answers `401` when the
57
+ * credential is dead) and only then decide the media is really gone.
58
+ * {@link isMediaMissing} carries that warning at the point of use.
59
+ *
60
+ * ## The rate ceilings are not the same on the two routes, and that is the point
61
+ *
62
+ * `rack-attack`'s `GENERAL_EXEMPT_PATHS` matches `/media/:id/data` and
63
+ * `/fs_nodes/:id/(data|zip)`, and matches NEITHER `data_url`:
64
+ *
65
+ * - **`data` is EXEMPT** from the 600/min authenticated and 120/min anonymous
66
+ * ceilings, on both the canonical route and the alias. An artwork grid, a
67
+ * prefetch sweep and a bulk download can hammer it without spending the
68
+ * account's budget. This is the route for images and for downloads.
69
+ * - **`data_url` COUNTS** against the general ceiling like any other call.
70
+ * Resolving a presigned URL per tile in a scrolling grid is how a client
71
+ * 429s itself out of the whole API. Reserve it for the player's resolver,
72
+ * which needs a URL a media element can load cross-origin, and cache what it
73
+ * returns BY MEDIA ID - never by URL, because a fresh signature comes back
74
+ * on every single resolve.
75
+ *
76
+ * The exemption is matched against a normalised path with the format suffix
77
+ * stripped, so `/media/48211/data.json` is exempt too, and a query string
78
+ * never changes the verdict.
79
+ *
80
+ * ## An OAuth access token cannot reach `/media/*` at all
81
+ *
82
+ * `MediaController` declares no `oauth_scope`, and `enforce_oauth_scope!`
83
+ * denies by omission: an OAuth token gets `403 {"error":"insufficient_scope"}`
84
+ * before the action runs. `FsNodesController` DOES declare `storage:read` on
85
+ * `data`/`data_url`, so - until `MediaController` grows a scope - a token
86
+ * holding `storage:read` can reach music bytes through the temporary alias and
87
+ * not through the canonical route. That inversion is a server-side gap rather
88
+ * than a design decision; it is the single reason to prefer the alias, and it
89
+ * is expected to close. A session token or the browser cookie reaches both.
90
+ *
91
+ * ## Who may read what
92
+ *
93
+ * Owner-only, with exactly one hole cut in it on purpose: the `audio`,
94
+ * `compressed_audio`, `artwork` and `compressed_artwork` attachments of a SONG
95
+ * that sits, unhidden, in a `friends`-visibility playlist belonging to one of
96
+ * the caller's friends. Stems, artist images and playlist covers stay
97
+ * owner-only. Every other cross-user surface in the API - jams, the friends
98
+ * feed, music profiles - ships READY-MADE presigned `artwork_url` / `audio_url`
99
+ * strings instead; use those verbatim and never try to re-derive one from an id
100
+ * you do not own, because that is a 404.
101
+ */
102
+ import { Resource } from "../http";
103
+ import type { FileOutput, RequestOptions } from "../types";
104
+ /**
105
+ * An `active_storage_attachments` id, as the blueprints serialise it: a string
106
+ * of digits.
107
+ *
108
+ * Typed as a plain `string` rather than a branded type because that is what
109
+ * every `*_media_id` field on a song, an artist and a playlist already is, and
110
+ * a brand would force a cast at every one of them.
111
+ */
112
+ export type MediaId = string;
113
+ /**
114
+ * How long a presigned URL from {@link MediaNamespace.dataUrl} stays valid:
115
+ * six hours (`MediaUrls::EXPIRY`).
116
+ *
117
+ * The window is long because it has to be, not out of generosity. A media
118
+ * element re-requests the object on every seek and whenever it resumes a
119
+ * buffered track, so the signing default of five minutes dies mid-listen with
120
+ * no way to recover. Six hours is also what the cross-user presigned URLs on
121
+ * jams and the friends feed are signed for.
122
+ */
123
+ export declare const MEDIA_URL_TTL_MS: number;
124
+ /**
125
+ * How long a browser may reuse the `302` from `GET /media/:id/data`:
126
+ * five minutes (`MusicMediaServing::REDIRECT_CACHE_TTL`), `Cache-Control:
127
+ * private`.
128
+ *
129
+ * It exists because a redirect with no `Cache-Control` is never cached, so
130
+ * every `<img>` re-followed the hop on every mount and artwork visibly
131
+ * re-fetched itself. Deliberately far below {@link MEDIA_URL_TTL_MS} so a
132
+ * cached redirect always points at a signature with hours of life left.
133
+ *
134
+ * `private` means per-browser: a shared cache must not hold it, because the
135
+ * response is the product of the caller's own credential.
136
+ */
137
+ export declare const MEDIA_REDIRECT_CACHE_TTL_MS: number;
138
+ /** The `media` namespace, reachable as `oms.media`. */
139
+ export declare class MediaNamespace extends Resource {
140
+ /**
141
+ * Absolute URL of `GET /media/:id/data`, carrying NO credential.
142
+ *
143
+ * Synchronous, because it is called while a component renders and a token
144
+ * provider may be async. What that means depends on the client:
145
+ *
146
+ * - **browser, cookie mode**: this is the URL you want. The `oms_session`
147
+ * cookie rides along on its own, the element follows the `302` to object
148
+ * storage with no CORS check at all, and the redirect is cacheable for
149
+ * {@link MEDIA_REDIRECT_CACHE_TTL_MS}. Do NOT put `crossorigin` on the
150
+ * element: it turns a no-cors load into a CORS one and re-creates exactly
151
+ * the failure the split between `data` and `data_url` exists to avoid.
152
+ * - **token mode (the native app, the CLI)**: this URL alone is a `404`,
153
+ * because no credential reaches the server. Use
154
+ * {@link authenticatedUrl} instead.
155
+ *
156
+ * Rate-limit exempt (see the namespace notes), which is what makes it the
157
+ * right route for an artwork grid and for a prefetch sweep.
158
+ */
159
+ url(id: MediaId): string;
160
+ /**
161
+ * {@link url} with the caller's token appended as `?token=`, for an `<img>`,
162
+ * an `<audio>`, a lock-screen artwork slot or a native downloader - anything
163
+ * that fetches a URL itself and cannot be given an `Authorization` header.
164
+ *
165
+ * Asynchronous because resolving the credential may refresh it, so it cannot
166
+ * be a getter. In cookie mode there is no token and it returns the bare URL,
167
+ * which is the correct answer there.
168
+ *
169
+ * `Session.candidate_tokens` reads the `Authorization` header, then
170
+ * `params[:token]`, then the cookie, so a query token is a first-class
171
+ * credential on this route - and, unlike on the HTML pages served by this
172
+ * host, it is not a redirect-injection risk here because there is no page to
173
+ * render as somebody else.
174
+ *
175
+ * **THE RESULT IS A LIVE CREDENTIAL.** It goes into the DOM, into the server
176
+ * access log, into `Referer` and into anything that records URLs; anyone
177
+ * holding it holds the whole session until it is revoked. Build it at the
178
+ * moment of use, never store it, never log it, and never hand it to
179
+ * something outside your own app - fetch the bytes with {@link download} and
180
+ * pass a `blob:` URL instead.
181
+ *
182
+ * One consequence worth planning for: the token is part of the URL, so it is
183
+ * part of the browser HTTP cache key. Every sign-in invalidates the warmed
184
+ * artwork cache of a token-mode client. That costs one cold cache, never
185
+ * correctness, and there is no client-side fix.
186
+ *
187
+ * ```ts
188
+ * const src = await oms.media.authenticatedUrl(song.compressed_artwork_media_id);
189
+ * ```
190
+ */
191
+ authenticatedUrl(id: MediaId): Promise<string>;
192
+ /**
193
+ * `GET /media/:id/data_url` - the presigned object-store URL for these bytes,
194
+ * as JSON.
195
+ *
196
+ * This is the route a PLAYER wants. `data` answers a `302` to storage, and a
197
+ * cross-origin media request cannot survive that hop either way: sent with
198
+ * credentials the browser turns `Origin` into `null` after the redirect and
199
+ * the store's wildcard `Access-Control-Allow-Origin` is illegal for a
200
+ * credentialed request; sent without credentials the route 404s before it
201
+ * ever redirects. Splitting it in two removes the redirect from the media
202
+ * request entirely - this call carries the session and returns a URL, and
203
+ * the element then loads that URL from storage directly and anonymously.
204
+ *
205
+ * Three properties that decide how you cache it:
206
+ *
207
+ * - **it is DIFFERENT on every call.** A fresh signature per resolve. Cache
208
+ * by media id, never by URL, or a cache keyed on the string will miss
209
+ * every time and grow forever.
210
+ * - **it is good for {@link MEDIA_URL_TTL_MS}** (six hours) and then it is
211
+ * not. Re-resolve on a playback failure rather than treating one as fatal.
212
+ * - **it COUNTS against the 600/min authenticated ceiling**, unlike
213
+ * {@link url}. One resolve per track as it is about to play is the
214
+ * intended shape; one resolve per tile in a grid is not.
215
+ *
216
+ * Do not forward your own `Authorization` header when fetching the returned
217
+ * URL - the signature is the credential, and the object store rejects a
218
+ * request that carries both.
219
+ *
220
+ * @throws {OmsApiError} 404 `"Not found"` - which does NOT mean the media is
221
+ * gone. See {@link isMediaMissing} and the namespace notes.
222
+ * @throws {OmsAuthError} 403 `insufficient_scope` when the client
223
+ * authenticated with an OAuth access token; `MediaController` declares no
224
+ * scope, so no token reaches it.
225
+ */
226
+ dataUrl(id: MediaId, options?: RequestOptions): Promise<string>;
227
+ /**
228
+ * `GET /media/:id/data`, following the redirect and reading the bytes into
229
+ * memory.
230
+ *
231
+ * For Bun, a Worker and React Native, where nothing enforces CORS. In a
232
+ * BROWSER this cannot work and no amount of client code fixes it - the `302`
233
+ * goes to a different origin, and the two failure modes described on
234
+ * {@link dataUrl} apply here too. There, put {@link url} or
235
+ * {@link authenticatedUrl} in an element, or resolve with {@link dataUrl}
236
+ * and fetch that.
237
+ *
238
+ * Buffers everything. A lossless album track is a bad thing to pull through
239
+ * JavaScript on a phone: resolve a URL and hand it to the platform player or
240
+ * to a native downloader instead, both of which stream to disk. This method
241
+ * is for artwork and for the odd file a host really does need in memory.
242
+ *
243
+ * The server falls back to sending the bytes inline (with a
244
+ * `Content-Disposition` filename) when the storage service has no URL host,
245
+ * which is what dev and test look like; both shapes arrive here identically.
246
+ *
247
+ * The runtime follows the redirect, and every conformant one drops the
248
+ * `Authorization` header on the cross-origin hop. That is not a limitation to
249
+ * work around: the signature in the presigned URL is the credential, and the
250
+ * object store rejects a request that arrives carrying both. A hand-rolled
251
+ * downloader that helpfully re-attaches the header will get a `400` from
252
+ * storage on bytes the server was perfectly willing to hand over.
253
+ *
254
+ * @throws {OmsError} `unsupported` when the redirect was blocked, which in
255
+ * practice means a browser in cookie mode.
256
+ * @throws {OmsApiError} 404 - see {@link isMediaMissing} before believing it.
257
+ */
258
+ download(id: MediaId, options?: RequestOptions): Promise<FileOutput>;
259
+ /**
260
+ * `GET /fs_nodes/:id/data` - the TEMPORARY numeric-id alias for {@link url}.
261
+ *
262
+ * Identical bytes, identical owner-or-404 rule, identical rate-limit
263
+ * exemption, resolved by the same `MusicMediaServing` concern. It is here for
264
+ * two reasons and no others: code ported from the old web frontend still
265
+ * spells it this way, and it is currently the only media route an OAuth token
266
+ * carrying `storage:read` can reach (see the namespace notes).
267
+ *
268
+ * The id must be all digits. `FsNodesController#data` only takes the media
269
+ * branch for `/\A\d+\z/`; anything else is looked up as a storage node,
270
+ * which for a media id means a 404 with a completely different cause.
271
+ * {@link isMediaId} checks that before you spend a request finding out.
272
+ *
273
+ * @deprecated Prefer {@link url}. The backend calls this alias temporary and
274
+ * it will be removed together with the `*_fs_node_id` blueprint twins.
275
+ */
276
+ aliasUrl(id: MediaId): string;
277
+ /**
278
+ * `GET /fs_nodes/:id/data_url` - the TEMPORARY numeric-id alias for
279
+ * {@link dataUrl}, with every caveat that method carries.
280
+ *
281
+ * Counts against the rate ceiling exactly as the canonical route does: the
282
+ * exemption covers `/fs_nodes/:id/data` and `/fs_nodes/:id/zip`, and stops
283
+ * there.
284
+ *
285
+ * Note the asymmetry with {@link aliasUrl}: a NON-numeric id here is not an
286
+ * error but a different endpoint - the storage node's own data URL - which is
287
+ * a perfectly valid thing to want and is what `oms.storage.downloadUrl` is
288
+ * for. Do not use this method to reach a storage node; use that one.
289
+ *
290
+ * @deprecated Prefer {@link dataUrl}.
291
+ */
292
+ aliasDataUrl(id: MediaId, options?: RequestOptions): Promise<string>;
293
+ }
294
+ /**
295
+ * Whether a value has the shape the `/fs_nodes` alias routes to media: one or
296
+ * more digits and nothing else.
297
+ *
298
+ * Worth having because the alias branches on exactly this regex. Hand it a
299
+ * storage uuid and it does not fail loudly - it quietly serves a different
300
+ * resource, or 404s for a reason that has nothing to do with the media you
301
+ * asked for.
302
+ */
303
+ export declare function isMediaId(value: unknown): value is MediaId;
304
+ /**
305
+ * Whether an error is a media `404`.
306
+ *
307
+ * **Read the name as "the server would not serve this", never as "this does
308
+ * not exist".** The media routes are `allow_unauthenticated_access` and
309
+ * `resolve_music_attachment` collapses five different situations into the same
310
+ * `404`, on purpose, so that the existence of a file never leaks: unknown id,
311
+ * wrong owner, non-music attachment, no credential, and a credential that has
312
+ * expired.
313
+ *
314
+ * The last two are the dangerous ones. A client that deletes its cached row,
315
+ * or renders a permanent placeholder, on the strength of this returning `true`
316
+ * will erase a user's entire visible library the first time their session
317
+ * lapses - with no `401` anywhere to explain it.
318
+ *
319
+ * The safe shape:
320
+ *
321
+ * ```ts
322
+ * try {
323
+ * src = await oms.media.dataUrl(song.compressed_audio_media_id);
324
+ * } catch (error) {
325
+ * if (!isMediaMissing(error)) throw error;
326
+ * // 404 is ambiguous. Ask something that CAN answer 401 before believing it.
327
+ * await oms.account.me(); // throws OmsAuthError 401 on a dead session
328
+ * markArtworkUnavailable(song); // only now is "gone" a fair conclusion
329
+ * }
330
+ * ```
331
+ */
332
+ export declare function isMediaMissing(error: unknown): boolean;
333
+ /**
334
+ * The first media id in the list that is actually present, or `null`.
335
+ *
336
+ * Every music record offers the same choice twice over: a compressed twin and
337
+ * an original, either of which may be `null`. The compressed one is what a
338
+ * client should reach for - the originals are lossless files on a Raspberry Pi
339
+ * and an album grid that asks for them takes seconds per tile - and the
340
+ * fallback chain is written out by hand in every client today.
341
+ *
342
+ * ```ts
343
+ * const artwork = firstMediaId(song.compressed_artwork_media_id, song.artwork_media_id);
344
+ * const audio = firstMediaId(song.compressed_audio_media_id, song.audio_media_id);
345
+ * ```
346
+ *
347
+ * Empty strings are treated as absent: a blueprint field is `null` when there
348
+ * is no attachment, but a form round-trip through a URL or a database can turn
349
+ * that into `""`, and an empty id would build a request for `/media//data`.
350
+ */
351
+ export declare function firstMediaId(...ids: readonly (MediaId | null | undefined)[]): MediaId | null;