@omelhorsite/sdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +4939 -552
- package/dist/types/client.d.ts +60 -3
- package/dist/types/http.d.ts +444 -19
- package/dist/types/index.d.ts +4 -1
- package/dist/types/resources/account.d.ts +66 -3
- package/dist/types/resources/admin.d.ts +1837 -0
- package/dist/types/resources/auth/index.d.ts +39 -0
- package/dist/types/resources/auth/passkeys.d.ts +652 -0
- package/dist/types/resources/auth/sessions.d.ts +847 -0
- package/dist/types/resources/chests.d.ts +54 -3
- package/dist/types/resources/content.d.ts +2970 -0
- package/dist/types/resources/dynamicQrs.d.ts +39 -3
- package/dist/types/resources/forms.d.ts +176 -35
- package/dist/types/resources/index.d.ts +19 -8
- package/dist/types/resources/ipLookup.d.ts +20 -4
- package/dist/types/resources/jobs.d.ts +62 -21
- package/dist/types/resources/library.d.ts +1435 -0
- package/dist/types/resources/linkTrees.d.ts +142 -30
- package/dist/types/resources/media.d.ts +351 -0
- package/dist/types/resources/movies.d.ts +1186 -0
- package/dist/types/resources/music/artists.d.ts +1066 -0
- package/dist/types/resources/music/imports.d.ts +940 -0
- package/dist/types/resources/music/index.d.ts +61 -0
- package/dist/types/resources/music/playlists.d.ts +1026 -0
- package/dist/types/resources/music/social.d.ts +1132 -0
- package/dist/types/resources/music/songs.d.ts +1183 -0
- package/dist/types/resources/notepads.d.ts +4 -1
- package/dist/types/resources/quotas.d.ts +7 -1
- package/dist/types/resources/realtime.d.ts +855 -0
- package/dist/types/resources/shortLinks.d.ts +45 -4
- package/dist/types/resources/social.d.ts +1330 -0
- package/dist/types/resources/storage/upload.d.ts +158 -11
- package/dist/types/resources/storage.d.ts +88 -22
- package/dist/types/resources/tickets.d.ts +82 -3
- package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
- package/dist/types/resources/tools/captions.d.ts +448 -21
- package/dist/types/resources/tools/downloader.d.ts +21 -0
- package/dist/types/resources/tools/index.d.ts +57 -15
- package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
- package/dist/types/resources/tools/transcription.d.ts +35 -13
- package/dist/types/resources/tools/upscale.d.ts +23 -3
- package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
- package/dist/types/types.d.ts +249 -17
- package/package.json +2 -1
|
@@ -10,26 +10,56 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { Resource } from "../http";
|
|
12
12
|
import type { BaseRecord, FileInput, Id, RequestOptions, Timestamp } from "../types";
|
|
13
|
-
/**
|
|
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
|
-
* `
|
|
23
|
-
*
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
83
|
-
*
|
|
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
|
|
90
|
-
readonly avatar_data_url
|
|
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
|
-
*
|
|
100
|
-
* and ending alphanumeric, and never one
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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?:
|
|
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
|
-
/**
|
|
175
|
-
|
|
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;
|