@omelhorsite/sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +112 -130
  2. package/dist/index.js +4800 -610
  3. package/dist/types/client.d.ts +60 -3
  4. package/dist/types/http.d.ts +444 -19
  5. package/dist/types/index.d.ts +5 -1
  6. package/dist/types/internal/attachments.d.ts +17 -0
  7. package/dist/types/internal/helpers.d.ts +9 -0
  8. package/dist/types/listing.d.ts +90 -0
  9. package/dist/types/resources/account.d.ts +71 -5
  10. package/dist/types/resources/admin/authorizedApplications.d.ts +133 -0
  11. package/dist/types/resources/admin/chests.d.ts +51 -0
  12. package/dist/types/resources/admin/eventAlerts.d.ts +60 -0
  13. package/dist/types/resources/admin/identities.d.ts +99 -0
  14. package/dist/types/resources/admin/index.d.ts +77 -0
  15. package/dist/types/resources/admin/jobs.d.ts +97 -0
  16. package/dist/types/resources/admin/myOauthApplications.d.ts +282 -0
  17. package/dist/types/resources/admin/notepads.d.ts +49 -0
  18. package/dist/types/resources/admin/oauthApplications.d.ts +309 -0
  19. package/dist/types/resources/admin/quotas.d.ts +114 -0
  20. package/dist/types/resources/admin/shortLinks.d.ts +201 -0
  21. package/dist/types/resources/admin/types.d.ts +222 -0
  22. package/dist/types/resources/admin/vocalSeparations.d.ts +98 -0
  23. package/dist/types/resources/admin.d.ts +1841 -0
  24. package/dist/types/resources/auth/index.d.ts +39 -0
  25. package/dist/types/resources/auth/passkeys.d.ts +652 -0
  26. package/dist/types/resources/auth/sessions.d.ts +844 -0
  27. package/dist/types/resources/chests.d.ts +54 -3
  28. package/dist/types/resources/content/analysis.d.ts +62 -0
  29. package/dist/types/resources/content/blogs.d.ts +471 -0
  30. package/dist/types/resources/content/feedbacks.d.ts +263 -0
  31. package/dist/types/resources/content/index.d.ts +52 -0
  32. package/dist/types/resources/content/intel/articles.d.ts +230 -0
  33. package/dist/types/resources/content/intel/config.d.ts +135 -0
  34. package/dist/types/resources/content/intel/index.d.ts +53 -0
  35. package/dist/types/resources/content/intel/items.d.ts +91 -0
  36. package/dist/types/resources/content/intel/reports.d.ts +108 -0
  37. package/dist/types/resources/content/intel/scripts.d.ts +152 -0
  38. package/dist/types/resources/content/intel/sources.d.ts +236 -0
  39. package/dist/types/resources/content/intel/stats.d.ts +105 -0
  40. package/dist/types/resources/content/intel/types.d.ts +86 -0
  41. package/dist/types/resources/content/jokes.d.ts +93 -0
  42. package/dist/types/resources/content/notifications.d.ts +147 -0
  43. package/dist/types/resources/content/serviceUsages.d.ts +73 -0
  44. package/dist/types/resources/content/servicesStatus.d.ts +194 -0
  45. package/dist/types/resources/content/siteConfig.d.ts +43 -0
  46. package/dist/types/resources/content/spaceInvaders.d.ts +154 -0
  47. package/dist/types/resources/content.d.ts +2775 -0
  48. package/dist/types/resources/dynamicQrs.d.ts +39 -3
  49. package/dist/types/resources/forms.d.ts +176 -35
  50. package/dist/types/resources/index.d.ts +19 -8
  51. package/dist/types/resources/ipLookup.d.ts +20 -4
  52. package/dist/types/resources/jobs.d.ts +69 -25
  53. package/dist/types/resources/library/annotations.d.ts +162 -0
  54. package/dist/types/resources/library/books.d.ts +534 -0
  55. package/dist/types/resources/library/chat.d.ts +259 -0
  56. package/dist/types/resources/library/index.d.ts +39 -0
  57. package/dist/types/resources/library/shelves.d.ts +237 -0
  58. package/dist/types/resources/library/types.d.ts +120 -0
  59. package/dist/types/resources/library.d.ts +1433 -0
  60. package/dist/types/resources/linkTrees.d.ts +142 -30
  61. package/dist/types/resources/media.d.ts +351 -0
  62. package/dist/types/resources/movies/addons.d.ts +388 -0
  63. package/dist/types/resources/movies/collections.d.ts +352 -0
  64. package/dist/types/resources/movies/index.d.ts +29 -0
  65. package/dist/types/resources/movies/types.d.ts +50 -0
  66. package/dist/types/resources/movies/watchProgress.d.ts +277 -0
  67. package/dist/types/resources/movies.d.ts +1160 -0
  68. package/dist/types/resources/music/artists.d.ts +1068 -0
  69. package/dist/types/resources/music/imports.d.ts +941 -0
  70. package/dist/types/resources/music/index.d.ts +61 -0
  71. package/dist/types/resources/music/playlists.d.ts +1027 -0
  72. package/dist/types/resources/music/social.d.ts +1132 -0
  73. package/dist/types/resources/music/songs.d.ts +1167 -0
  74. package/dist/types/resources/notepads.d.ts +4 -1
  75. package/dist/types/resources/quotas.d.ts +7 -1
  76. package/dist/types/resources/realtime.d.ts +855 -0
  77. package/dist/types/resources/shortLinks.d.ts +49 -5
  78. package/dist/types/resources/social/groupChats.d.ts +458 -0
  79. package/dist/types/resources/social/index.d.ts +19 -0
  80. package/dist/types/resources/social/messages.d.ts +424 -0
  81. package/dist/types/resources/social/relationships.d.ts +322 -0
  82. package/dist/types/resources/social/types.d.ts +37 -0
  83. package/dist/types/resources/social.d.ts +1348 -0
  84. package/dist/types/resources/storage/upload.d.ts +158 -11
  85. package/dist/types/resources/storage.d.ts +111 -27
  86. package/dist/types/resources/tickets.d.ts +101 -13
  87. package/dist/types/resources/tools/backgroundRemoval.d.ts +18 -3
  88. package/dist/types/resources/tools/captions.d.ts +448 -21
  89. package/dist/types/resources/tools/downloader.d.ts +21 -0
  90. package/dist/types/resources/tools/index.d.ts +57 -15
  91. package/dist/types/resources/tools/jumpstyle.d.ts +50 -17
  92. package/dist/types/resources/tools/transcription.d.ts +35 -13
  93. package/dist/types/resources/tools/upscale.d.ts +23 -3
  94. package/dist/types/resources/tools/vocalSeparation.d.ts +30 -13
  95. package/dist/types/types.d.ts +249 -17
  96. package/package.json +2 -1
@@ -141,11 +141,62 @@ export declare class ChestEntriesNamespace extends Resource {
141
141
  /**
142
142
  * `GET /chest_entries/:id/data` - the entry's bytes.
143
143
  *
144
- * Answers 302 towards object storage and `fetch` follows it. Note that this
145
- * endpoint checks nothing at all: the entry id alone is enough to download
146
- * it, with or without the chest name or token.
144
+ * SENT WITH NO CREDENTIAL, deliberately. `ChestEntriesController` lists
145
+ * `data` in `allow_unauthenticated_access`, and the action itself is three
146
+ * lines: find the entry by id, check that something is attached, redirect.
147
+ * It never looks at the chest name, the chest token, or who is asking. The
148
+ * ENTRY ID IS THE WHOLE CAPABILITY - which is worth knowing for its own sake,
149
+ * and which also means a credential on this request could not possibly change
150
+ * the answer.
151
+ *
152
+ * That matters because sending one breaks the call in a browser. The action
153
+ * answers `302` to `minio.omelhorsite.pt`, and following that hop replaces the
154
+ * request's origin with an opaque one (Fetch standard: a cross-origin
155
+ * redirect of a CORS request whose origin already differs from the current
156
+ * URL's origin), so the store sees `Origin: null` and answers
157
+ * `Access-Control-Allow-Origin: *`. Wildcard plus credentials is illegal, so a
158
+ * client built with `sessionCookie: true` - the production web app - would
159
+ * have the browser reject the bytes with an opaque "Failed to fetch". Asking
160
+ * anonymously sidesteps it: `*` is fine for an uncredentialed request.
161
+ *
162
+ * Two shapes come back and both are handled here. Against MinIO it is the
163
+ * `302`. Against a Disk service (dev, test) presigning raises `ArgumentError`
164
+ * and the controller falls back to `send_data`, so the bytes arrive inline
165
+ * from Rails with a `Content-Disposition`. Either way this returns the bytes.
166
+ *
167
+ * Going around the transport costs the usual thing: no retry, no per-call
168
+ * deadline, only the caller's `signal`. Use {@link downloadUrl} when the
169
+ * destination is an `<a download>` or a media element rather than memory.
170
+ *
171
+ * @throws {OmsApiError} 404 when the entry is unknown, its bytes never
172
+ * landed, or the chest has passed its two-hour expiry and been swept.
147
173
  */
148
174
  download(id: Id, options?: RequestOptions): Promise<Blob>;
175
+ /**
176
+ * Absolute URL for an entry's bytes, for an `<a download>`, a `<video>`, or a
177
+ * new tab.
178
+ *
179
+ * Synchronous and credential-free, because the endpoint is: the entry id is
180
+ * the only thing it checks. The frontend's older helper appended a `?token=`
181
+ * here; this deliberately does not, because a session token in a URL that
182
+ * ends up in markup, in a shared link and in an access log buys precisely
183
+ * nothing on a route that never reads it.
184
+ *
185
+ * Treat the URL as a bearer capability all the same. Anyone holding it can
186
+ * pull the file until the chest expires, so it is exactly as shareable as the
187
+ * chest name and no more.
188
+ *
189
+ * ```tsx
190
+ * <a href={oms.chests.entries.downloadUrl(entry.id)} download={entry.name}>
191
+ * {entry.name}
192
+ * </a>
193
+ * ```
194
+ *
195
+ * A link is also the better answer for a large entry: the browser streams it
196
+ * straight to disk, where {@link download} would buffer the whole file in
197
+ * memory first.
198
+ */
199
+ downloadUrl(id: Id): string;
149
200
  /**
150
201
  * `DELETE /chest_entries/:id` - removes an entry and gives its bytes back to
151
202
  * the chest's ceiling.
@@ -0,0 +1,62 @@
1
+ /** Admin storage reports. */
2
+ import { Resource } from "../../http";
3
+ import type { RequestOptions } from "../../types";
4
+ import type { FsNode } from "../storage";
5
+ /** One day in a daily-count series. */
6
+ export interface AnalysisDailyPoint {
7
+ /** `YYYY-MM-DD`, server timezone. */
8
+ readonly date: string;
9
+ /** Rows created that day. `0` for a day with none - the series has no gaps. */
10
+ readonly count: number;
11
+ }
12
+ /** Days covered by {@link AnalysisNamespace.filesDaily}. Mirrors `DAILY_WINDOW_DAYS`. */
13
+ export declare const ANALYSIS_DAILY_WINDOW_DAYS = 30;
14
+ /**
15
+ * The `analysis` namespace: two admin reports about storage.
16
+ *
17
+ * **Admin only, and the refusal is unusual.** `require_admin!` answers `403`
18
+ * whose body is a long quotation from Monster House rather than an error code,
19
+ * so do not try to match on the message - check the status. An anonymous
20
+ * caller is stopped earlier, by the authentication filter, with the ordinary
21
+ * `401 "Session required to access this resource."`.
22
+ *
23
+ * Paths like `GET /analysis` and `GET /analysis/:id` exist in the router with
24
+ * no action behind them: calling one is a `404` carrying an HTML error page
25
+ * rather than this API's usual bare string. There are exactly two usable
26
+ * routes here and they are both below.
27
+ */
28
+ export declare class AnalysisNamespace extends Resource {
29
+ /**
30
+ * `GET /analysis/storages` - every root directory in the system, with its
31
+ * recursive size.
32
+ *
33
+ * Every node with no parent, for every user, which in practice means each
34
+ * account's home, trash and vault roots. Rendered in the DEFAULT view, which
35
+ * is what {@link FsNode} describes.
36
+ *
37
+ * Two things to expect:
38
+ *
39
+ * - **No owner.** The payload does not carry `user_id`, so it tells you
40
+ * that a root called `"home"` holds 40 GB and not whose it is.
41
+ * Correlating means another query.
42
+ * - **No limit and no paging.** The scope is unbounded, so the response
43
+ * grows linearly with the number of accounts. It is an admin report, not
44
+ * something to poll.
45
+ *
46
+ * The `size` on a root is the recursive total maintained by the storage
47
+ * layer; it has drifted from the true sum before, so read it as an estimate.
48
+ */
49
+ storages(options?: RequestOptions): Promise<FsNode[]>;
50
+ /**
51
+ * `GET /analysis/files_daily` - files created per day over the last
52
+ * {@link ANALYSIS_DAILY_WINDOW_DAYS} days. Unwraps `{"creations_daily": [...]}`.
53
+ *
54
+ * Exactly 30 entries, oldest first, zero-filled: a day with no uploads is
55
+ * present with `count: 0`. The last entry is today and is partial.
56
+ *
57
+ * Counts `fs_nodes` of kind `file` by `DATE(created_at)`, so it measures
58
+ * node creation and not bytes - a folder copy that mints 50 000 nodes shows
59
+ * up here as 50 000 files.
60
+ */
61
+ filesDaily(options?: RequestOptions): Promise<AnalysisDailyPoint[]>;
62
+ }
@@ -0,0 +1,471 @@
1
+ /** Blogs, blog posts and subscriptions. */
2
+ import { type ApiClient, Resource } from "../../http";
3
+ import type { Id, RequestOptions, Timestamp } from "../../types";
4
+ /** Primary key of a blog. An INTEGER: `blogs` kept its auto-increment id. */
5
+ export type BlogId = number;
6
+ /** Primary key of a blog post. An integer, like the blog it hangs off. */
7
+ export type BlogPostId = number;
8
+ /** The author of a blog, embedded in {@link Blog}. Not a full user record. */
9
+ export interface BlogAuthor {
10
+ /** User id. A STRING, unlike every other id in this section. */
11
+ readonly id: Id;
12
+ readonly handle: string;
13
+ readonly name: string;
14
+ }
15
+ /**
16
+ * A blog: one per user, created lazily.
17
+ *
18
+ * Deliberately NOT a `BaseRecord`: the payload has `created_at` and NO
19
+ * `updated_at`. Do not reach for one.
20
+ */
21
+ export interface Blog {
22
+ readonly id: BlogId;
23
+ /**
24
+ * URL-safe handle of the blog, and the ONLY way to address it on the read
25
+ * routes. Matches `/\A[a-z0-9][a-z0-9_-]*\z/`, 1-64 characters, unique
26
+ * across the whole table. Defaults to the owner's handle, lowercased.
27
+ */
28
+ readonly slug: string;
29
+ /** Display name. Defaults to `"<name>'s blog"`. Up to 80 characters. */
30
+ readonly name: string;
31
+ /** Up to 240 characters, or `null`. */
32
+ readonly description: string | null;
33
+ /** Who owns it. One blog per user, enforced by a unique index on `user_id`. */
34
+ readonly user: BlogAuthor;
35
+ /**
36
+ * Subscribers with a `confirmed_at`, counted live on every render.
37
+ *
38
+ * In practice that is every subscriber: the only reachable way to subscribe
39
+ * requires a session, and a signed-in subscription is confirmed on the spot.
40
+ * See {@link BlogsNamespace.subscribe}.
41
+ */
42
+ readonly followers_count: number;
43
+ /** Posts with a `published_at`, counted live on every render. */
44
+ readonly published_posts_count: number;
45
+ readonly created_at: Timestamp;
46
+ /**
47
+ * Whether the CALLING user subscribes to this blog. Computed against the
48
+ * caller, so the same row differs per identity - never cache it across
49
+ * identities, and note it is `false` (not `null`) for an anonymous caller.
50
+ */
51
+ readonly is_following: boolean;
52
+ }
53
+ /**
54
+ * A post as it appears in a listing: the summary view.
55
+ *
56
+ * Like {@link Blog}, this is not a `BaseRecord`: the summary carries neither
57
+ * `created_at` nor `updated_at`. They appear only on {@link BlogPost}, the
58
+ * `:extended` view, which is this shape PLUS extras, never a subset.
59
+ */
60
+ export interface BlogPostSummary {
61
+ readonly id: BlogPostId;
62
+ /** URL-safe, unique within the blog. Derived from the title when omitted. */
63
+ readonly slug: string;
64
+ /** Up to 200 characters. */
65
+ readonly title: string;
66
+ /**
67
+ * Up to 240 characters, derived from the first characters of `content_md`
68
+ * with the markdown punctuation stripped, unless the author wrote one. The
69
+ * derivation runs in a `before_save`, so it is refreshed on every write
70
+ * where the excerpt is blank - and never once it is not.
71
+ */
72
+ readonly excerpt: string | null;
73
+ /** `null` for a draft. Presence of this field IS the published flag. */
74
+ readonly published_at: Timestamp | null;
75
+ /**
76
+ * Estimated reading time, recomputed on every save at 220 words per minute
77
+ * and floored at 1. Server-owned: sending it is ignored.
78
+ */
79
+ readonly reading_minutes: number;
80
+ /** Lowercased, de-duplicated, at most 10. Never `null` in the payload. */
81
+ readonly tags: string[];
82
+ /** The blog it belongs to, trimmed to three fields. */
83
+ readonly blog: {
84
+ readonly id: BlogId;
85
+ readonly slug: string;
86
+ readonly name: string;
87
+ };
88
+ }
89
+ /** A post with its body: the `:extended` view, returned by every single-post route. */
90
+ export interface BlogPost extends BlogPostSummary {
91
+ /**
92
+ * The markdown source, up to 200 000 characters.
93
+ *
94
+ * There is a `content_html` column next to it in the database, rendered on
95
+ * write - but no response exposes it, so the client renders the markdown
96
+ * itself.
97
+ */
98
+ readonly content_md: string | null;
99
+ readonly created_at: Timestamp;
100
+ readonly updated_at: Timestamp;
101
+ /** Whether the CALLING user may edit it. Per-viewer, like `is_following`. */
102
+ readonly is_owner: boolean;
103
+ }
104
+ /** A {@link Blog} plus the posts the caller may see. Returned by `show` and `mine`. */
105
+ export interface BlogWithPosts extends Blog {
106
+ /**
107
+ * Newest first (`published_at DESC, created_at DESC`).
108
+ *
109
+ * NOT paginated and NOT capped: `blog.blog_posts.viewable_by(...).recent`
110
+ * runs with no `limit`, so a blog with a thousand posts returns a thousand
111
+ * summaries in one response. This is the one listing in the file with no
112
+ * ceiling of any kind.
113
+ *
114
+ * On `show` this is published posts only, unless the caller owns the blog,
115
+ * in which case drafts are included too. On `mine` it is every post,
116
+ * published or not.
117
+ */
118
+ readonly posts: BlogPostSummary[];
119
+ }
120
+ /** Fields {@link BlogsNamespace.updateMine} may change. */
121
+ export interface UpdateBlogInput {
122
+ /**
123
+ * New slug. Must match `/\A[a-z0-9][a-z0-9_-]*\z/` (1-64 chars) and be free
124
+ * across the whole table, or the call is `400`. Changing it BREAKS every
125
+ * link already published against the old one: there is no redirect and no
126
+ * history table.
127
+ */
128
+ readonly slug?: string;
129
+ /** Up to 80 characters. */
130
+ readonly name?: string;
131
+ /** Up to 240 characters. */
132
+ readonly description?: string | null;
133
+ }
134
+ /** Result of a subscribe call. */
135
+ export interface BlogSubscribeResult {
136
+ readonly ok: boolean;
137
+ /**
138
+ * `true` when the subscription is live.
139
+ *
140
+ * In practice it is ALWAYS `true` today. The field exists because the
141
+ * controller can also create an unconfirmed, email-only subscription - but
142
+ * that branch is unreachable over HTTP (see
143
+ * {@link BlogsNamespace.subscribe}), so every row this route can actually
144
+ * create is a signed-in one, and `set_confirmed_at_for_user_subs` confirms
145
+ * those on insert.
146
+ *
147
+ * Were a `false` ever to reach you, there would be nothing to do about it:
148
+ * the row carries an `unsubscribe_token`, but no route in the application
149
+ * reads it, so there is no confirmation step to complete.
150
+ */
151
+ readonly confirmed: boolean;
152
+ }
153
+ /**
154
+ * The `blogs` namespace: one blog per user, markdown posts, and a subscriber
155
+ * list.
156
+ *
157
+ * The read routes address a blog by its SLUG, never by its id - the lookup is
158
+ * by lowercased slug and nothing else, so passing the numeric id gets
159
+ * `404 "Blog not found"`. The two subscribe routes are the exception: they try
160
+ * the slug first and then fall back to the id, so they accept either.
161
+ *
162
+ * Reads (`GET /blogs`, `GET /blogs/:slug`, `GET /blog_posts`,
163
+ * `GET /blog_posts/:id`, `GET /blogs/:blog/posts/:slug`) are open to anonymous
164
+ * callers; everything else needs a session.
165
+ */
166
+ export declare class BlogsNamespace extends Resource {
167
+ /** Posts, blog metadata and publishing. Also mounted as `oms.blogPosts`. */
168
+ readonly posts: BlogPostsNamespace;
169
+ constructor(http: ApiClient);
170
+ /**
171
+ * `GET /blogs` - the discovery feed: the 30 most recent PUBLISHED posts
172
+ * across every blog on the site.
173
+ *
174
+ * Despite the path this returns POSTS, not blogs, and there is no endpoint
175
+ * anywhere that lists blogs. The wire shape is `{"posts": [...]}`, one of
176
+ * the two envelopes in this file; the array is unwrapped here.
177
+ *
178
+ * Fixed at 30 rows, newest first. No paging, no filters, no `search` - the
179
+ * action is `BlogPost.published.recent.limit(30)` and reads nothing off the
180
+ * query string, so anything you add to it is ignored rather than rejected.
181
+ * To go deeper than 30, there is nothing to page: this is a front page, not
182
+ * an archive.
183
+ *
184
+ * Anonymous-safe, and drafts never leak into it regardless of who asks.
185
+ */
186
+ discover(options?: RequestOptions): Promise<BlogPostSummary[]>;
187
+ /**
188
+ * `GET /blogs/:slug` - one blog with all of its visible posts.
189
+ *
190
+ * The slug is lowercased by the server before the lookup, so case does not
191
+ * matter. A numeric id does NOT work here; use the slug.
192
+ *
193
+ * Published posts only, unless the caller OWNS the blog, in which case their
194
+ * drafts are included. `BlogPost.viewable_by` keys on `blog.user_id` alone,
195
+ * so an admin looking at somebody else's blog sees exactly what the public
196
+ * sees. The `posts` array is unbounded - see {@link BlogWithPosts.posts}.
197
+ *
198
+ * @throws {OmsApiError} 404 `"Blog not found"`.
199
+ */
200
+ show(slug: string, options?: RequestOptions): Promise<BlogWithPosts>;
201
+ /**
202
+ * `GET /blogs/mine` - the caller's own blog, drafts included.
203
+ *
204
+ * **This read has a side effect.** `Blog.find_or_create_for` CREATES the
205
+ * blog row on first call, with the slug defaulted to the caller's handle and
206
+ * the name defaulted to `"<name>'s blog"`, and the creation fires a Discord
207
+ * `blog_created` alert. So "does this user have a blog" is not a question
208
+ * this endpoint can answer - by the time it replies, they do. Call it when
209
+ * the user opens their blog dashboard, not to probe.
210
+ *
211
+ * The default slug is the user's handle, which can collide with a blog
212
+ * somebody already owns under that slug - handles and blog slugs are
213
+ * separate namespaces and nothing keeps them apart. `create!` then raises
214
+ * `ActiveRecord::RecordInvalid`, which reaches the caller as a `422` with a
215
+ * Rails error page rather than this API's usual bare string, and fires a
216
+ * Discord error alert on the way out. Rare, and unfixable from the client:
217
+ * the endpoint takes no arguments.
218
+ *
219
+ * @throws {OmsApiError} 401 `"Session required to access this resource."`.
220
+ * The action also carries its own `unauthorized!("Not authenticated")`
221
+ * guard, but `allow_unauthenticated_access` covers only `index` and `show`,
222
+ * so the framework filter fires first and that message never ships.
223
+ */
224
+ mine(options?: RequestOptions): Promise<BlogWithPosts>;
225
+ /**
226
+ * `PATCH /blogs/mine` - renames or re-slugs the caller's blog.
227
+ *
228
+ * Creates the blog first if there is none, exactly like {@link mine}, so
229
+ * this can be the very first call a client makes.
230
+ *
231
+ * Answers the blog ALONE - no `posts` key, unlike every other blog route.
232
+ * That asymmetry is the reason this returns {@link Blog} and not
233
+ * {@link BlogWithPosts}.
234
+ *
235
+ * Only `slug`, `name` and `description` are permitted; anything else in the
236
+ * body is dropped in silence. Re-slugging breaks published links - see
237
+ * {@link UpdateBlogInput.slug}.
238
+ *
239
+ * @throws {OmsApiError} 400 with the validation sentence when the slug is
240
+ * taken or malformed; 401 `"Session required to access this resource."`.
241
+ */
242
+ updateMine(input: UpdateBlogInput, options?: RequestOptions): Promise<Blog>;
243
+ /**
244
+ * `POST /blogs/:slug/subscribe` - follows a blog.
245
+ *
246
+ * **Signed-in callers only, despite appearances.** The action reads like it
247
+ * supports anonymous email subscriptions - there is a whole branch for it,
248
+ * ending in `400 "Email required for anonymous subscribe"` - but
249
+ * `allow_unauthenticated_access` covers only `index` and `show`, so
250
+ * `require_authentication` rejects an anonymous caller with
251
+ * `401 "Session required to access this resource."` long before that branch
252
+ * runs. The email path is dead code today.
253
+ *
254
+ * Which means `email` is effectively ignored: with a session present the
255
+ * controller always takes the user branch, the subscription is attached to
256
+ * the account, and it is confirmed on insert, so `confirmed` is always
257
+ * `true` and `followers_count` moves. The parameter is kept here because the
258
+ * server accepts it and because the branch could be revived by one line in
259
+ * the controller - not because sending it changes anything today.
260
+ *
261
+ * Idempotent by construction: `find_or_initialize_by` on
262
+ * `(blog, user, email)` means subscribing twice is a no-op that answers
263
+ * `200` both times, so this is one of the few POSTs here where a retry
264
+ * cannot duplicate anything.
265
+ *
266
+ * Accepts either the slug or the numeric id in the path - this route and its
267
+ * `DELETE` twin are the only ones in the file that do.
268
+ *
269
+ * @throws {OmsApiError} 404 `"Blog not found"`; 401 without a session.
270
+ */
271
+ subscribe(slugOrId: string | BlogId, input?: {
272
+ readonly email?: string;
273
+ }, options?: RequestOptions): Promise<BlogSubscribeResult>;
274
+ /**
275
+ * `DELETE /blogs/:slug/subscribe` - unfollows a blog.
276
+ *
277
+ * Signed-in callers only, and it removes only the CALLER's own subscription:
278
+ * the scope is `where(blog:, user: Current.user)`, so an email-only row
279
+ * (were one to exist) could not be removed through here at all.
280
+ *
281
+ * Answers `{"ok": true}` whether or not a subscription existed - it is a
282
+ * `delete_all` on a scope, so "not subscribed" and "unsubscribed" are the
283
+ * same answer, and a double call is harmless.
284
+ *
285
+ * @throws {OmsApiError} 404 `"Blog not found"`; 401
286
+ * `"Session required to access this resource."`.
287
+ */
288
+ unsubscribe(slugOrId: string | BlogId, options?: RequestOptions): Promise<{
289
+ ok: boolean;
290
+ }>;
291
+ }
292
+ /** Arguments for {@link BlogPostsNamespace.create}. */
293
+ export interface CreateBlogPostInput {
294
+ /** Required, up to 200 characters. */
295
+ readonly title: string;
296
+ /**
297
+ * URL-safe slug, unique within the blog. Omit it and the server derives one
298
+ * from the title (lowercased, non-alphanumerics collapsed to `-`, trimmed to
299
+ * 80 characters), falling back to `post-<6 hex>` when the title has no
300
+ * alphanumerics at all - a title written entirely in a non-Latin script
301
+ * therefore gets a random slug, not a transliterated one.
302
+ */
303
+ readonly slug?: string;
304
+ /** Markdown source, up to 200 000 characters. */
305
+ readonly content_md?: string;
306
+ /**
307
+ * Up to 280 characters by the model, but only 240 are ever written by the
308
+ * derivation. Leave it out and the server writes the first 240 characters of
309
+ * the stripped markdown; it is re-derived on every save where it is blank,
310
+ * so clearing it back to `""` re-enables the automatic one.
311
+ */
312
+ readonly excerpt?: string;
313
+ /**
314
+ * Up to 10 tags. Lowercased, trimmed, de-duplicated and blank-filtered by
315
+ * the controller, and anything past the tenth is dropped in silence.
316
+ */
317
+ readonly tags?: readonly string[];
318
+ }
319
+ /**
320
+ * Arguments for {@link BlogPostsNamespace.update}.
321
+ *
322
+ * `tags` is optional in the type and DANGEROUS to omit. `post_params` ends
323
+ * with an unconditional `raw[:tags] = (raw[:tags] || []) ...`, so a PATCH that
324
+ * does not mention tags assigns the EMPTY ARRAY over whatever the post had.
325
+ * There is no partial-update semantics for this field: resend the tags you
326
+ * already hold on every update, or watch them disappear. Same shape of bug as
327
+ * `manifest_json` on movie addons.
328
+ */
329
+ export interface UpdateBlogPostInput {
330
+ readonly title?: string;
331
+ /** Changing it breaks published links; uniqueness is scoped to the blog. */
332
+ readonly slug?: string;
333
+ readonly content_md?: string;
334
+ readonly excerpt?: string;
335
+ /** ALWAYS send this. Omitting it clears the post's tags. See the interface docs. */
336
+ readonly tags?: readonly string[];
337
+ }
338
+ /**
339
+ * Posts, reachable as `oms.content.blogs.posts`.
340
+ *
341
+ * A post lives under exactly one blog and a user has exactly one blog, so
342
+ * there is no "which blog" argument anywhere: {@link create} always writes to
343
+ * the caller's own, creating it if needed.
344
+ */
345
+ export declare class BlogPostsNamespace extends Resource {
346
+ /**
347
+ * `GET /blog_posts` - the 50 most recent posts the caller may see, newest
348
+ * first, optionally narrowed to one blog.
349
+ *
350
+ * Envelope: the wire shape is `{"posts": [...]}` and the array is unwrapped
351
+ * here.
352
+ *
353
+ * NOT the list DSL, despite the plural path. `blog_slug` is the only
354
+ * parameter the action reads; `search`, `exact_search` and `modifiers` are
355
+ * ignored rather than rejected, and the limit of 50 is not negotiable. There
356
+ * is no way to page past it, so this is a feed and not an archive - to walk
357
+ * a whole blog, read {@link BlogsNamespace.show}, whose `posts` array is
358
+ * uncapped.
359
+ *
360
+ * Visibility follows the caller: published posts always, plus the caller's
361
+ * OWN drafts. An anonymous caller sees published posts only.
362
+ *
363
+ * @param input.blogSlug Restrict to one blog. An unknown slug does NOT 404 -
364
+ * `find_by` returns nil and the action silently falls back to the
365
+ * site-wide listing, so a typo here returns everybody's posts instead of
366
+ * an empty list. Check the `blog` on each row if that distinction matters.
367
+ */
368
+ list(input?: {
369
+ readonly blogSlug?: string;
370
+ }, options?: RequestOptions): Promise<BlogPostSummary[]>;
371
+ /**
372
+ * `GET /blog_posts/:id` - one post by numeric id, with its body.
373
+ *
374
+ * @throws {OmsApiError} 404 `"Post not found"`; **401**
375
+ * `"Draft only visible to author"` when the post exists but is
376
+ * unpublished and the caller is not its author. Note that this is a 401
377
+ * rather than a 404, so it confirms that a draft with that id exists.
378
+ */
379
+ get(id: BlogPostId, options?: RequestOptions): Promise<BlogPost>;
380
+ /**
381
+ * `GET /blogs/:blogSlug/posts/:slug` - one post by the pair of slugs, which
382
+ * is the shape a public permalink has.
383
+ *
384
+ * **This route needs a session, and the id route does not.** That is almost
385
+ * certainly a mistake in the backend and it is worth knowing before you
386
+ * build a public permalink on it: an anonymous reader following a shared
387
+ * link gets `401 "Session required to access this resource."` here, while
388
+ * {@link get} hands them the very same published post. Until that is fixed,
389
+ * render public permalinks through {@link get} with the numeric id, or
390
+ * expect signed-in readers only.
391
+ *
392
+ * Both slugs are lowercased server-side before the lookup. The route is
393
+ * declared with `constraints: { blog_slug: /[^\/]+/, slug: /[^\/]+/ }`, so a
394
+ * slug containing a slash cannot reach it at all - not a concern for
395
+ * server-minted slugs, which are `[a-z0-9_-]` only.
396
+ *
397
+ * @throws {OmsApiError} 401 without a session, before anything else is
398
+ * checked; 404 `"Blog not found"` or `"Post not found"`; 401
399
+ * `"Draft only visible to author"`.
400
+ */
401
+ getBySlugs(blogSlug: string, slug: string, options?: RequestOptions): Promise<BlogPost>;
402
+ /**
403
+ * `POST /blog_posts` - writes a new post to the caller's own blog. `201`.
404
+ *
405
+ * There is no blog argument because there is no choice: the controller calls
406
+ * `Blog.find_or_create_for(Current.user)`, so this CREATES the caller's blog
407
+ * as a side effect on their very first post, exactly like
408
+ * {@link BlogsNamespace.mine}.
409
+ *
410
+ * The post starts as a DRAFT - `published_at` is not settable here and no
411
+ * amount of arguments will publish it. Publishing is a second call, and it
412
+ * is {@link setPublished}, not {@link update}.
413
+ *
414
+ * Rides the general ceiling: there is no per-user cap on how many posts may
415
+ * be created, and no length cap beyond the model's 200 000 characters of
416
+ * markdown.
417
+ *
418
+ * @throws {OmsApiError} 401 `"Session required to access this resource."`;
419
+ * 400 with the validation sentence, most often the slug already existing
420
+ * in this blog.
421
+ */
422
+ create(input: CreateBlogPostInput, options?: RequestOptions): Promise<BlogPost>;
423
+ /**
424
+ * `PATCH /blog_posts/:id` - edits a post's fields.
425
+ *
426
+ * Do NOT put `publish` in this body. The controller checks for it FIRST and,
427
+ * when it is a boolean or the string `"true"`/`"false"`, publishes or
428
+ * unpublishes and returns immediately - `ok!` raises the response - so every
429
+ * other field in the same request is discarded without a word. That is a
430
+ * silent data loss, not an error you can catch. Use {@link setPublished} for
431
+ * the flag and this method for the content; two calls, in either order.
432
+ *
433
+ * Always send `tags`, including when they have not changed. See
434
+ * {@link UpdateBlogPostInput}.
435
+ *
436
+ * Editing does not change `published_at`, so an edit to a published post
437
+ * stays published and does not move in the feed's ordering.
438
+ *
439
+ * @throws {OmsApiError} 404 `"Post not found"`; 401 `"Not your post"`; 400
440
+ * with the validation sentence.
441
+ */
442
+ update(id: BlogPostId, input: UpdateBlogPostInput, options?: RequestOptions): Promise<BlogPost>;
443
+ /**
444
+ * `PATCH /blog_posts/:id` with `{ publish }` - the publish switch, on its
445
+ * own.
446
+ *
447
+ * Separated from {@link update} because the controller treats it as an
448
+ * early-return branch rather than as a field: a body carrying `publish`
449
+ * never reaches `post.update(post_params)`, so mixing the two loses the
450
+ * content edit. Sending it alone is the only safe way to use it.
451
+ *
452
+ * `publish: true` stamps `published_at` with the current time - and moves
453
+ * the post to the top of every `recent` ordering. Re-publishing an already
454
+ * published post is a no-op that still answers `200` with the ORIGINAL
455
+ * `published_at`, so this cannot be used to bump a post. `publish: false`
456
+ * clears `published_at`, which unlists the post everywhere and, for a
457
+ * non-owner, turns {@link get} into a 401.
458
+ *
459
+ * @throws {OmsApiError} 404 `"Post not found"`; 401 `"Not your post"`.
460
+ */
461
+ setPublished(id: BlogPostId, publish: boolean, options?: RequestOptions): Promise<BlogPost>;
462
+ /**
463
+ * `DELETE /blog_posts/:id` - permanent. `204`, no body.
464
+ *
465
+ * Takes the attached `cover_image` with it (`dependent: :destroy`). There is
466
+ * no trash and no undo.
467
+ *
468
+ * @throws {OmsApiError} 404 `"Post not found"`; 401 `"Not your post"`.
469
+ */
470
+ destroy(id: BlogPostId, options?: RequestOptions): Promise<void>;
471
+ }