@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
@@ -0,0 +1,2970 @@
1
+ /**
2
+ * The `content` namespace: the small, public-facing corners of the API that do
3
+ * not belong to any of the big products.
4
+ *
5
+ * Ten endpoint families live here, and they have nothing in common except
6
+ * being too small to deserve a file each: blogs and their posts, the
7
+ * notification inbox, the feedback box, the joke table, the public config
8
+ * blob, the status page, the per-user "recent services" counters, two admin
9
+ * analysis reports, the Space Invaders leaderboard, and the read-only proxy in
10
+ * front of the intel sidecar. Each is its own class so the grouping stays an
11
+ * implementation detail: mount them wherever a host prefers.
12
+ *
13
+ * ## Nine things that have already cost bugs
14
+ *
15
+ * 1. **Half of these routes are NOT the list DSL.** `GET /blogs`,
16
+ * `GET /blog_posts`, `GET /space_invaders_games/leaderboard`,
17
+ * `GET /service_usages/top` and both `/analysis` reports are hand-written
18
+ * controller actions with a hard-coded `limit` and no paging at all. Only
19
+ * {@link NotificationsNamespace.list}, {@link JokesNamespace.list},
20
+ * {@link FeedbacksNamespace.list} and {@link SpaceInvadersNamespace.list}
21
+ * accept `search` / `exact_search` / `modifiers`.
22
+ * 2. **Two of the responses carry an envelope**, which almost nothing else in
23
+ * this API does: `GET /blogs` and `GET /blog_posts` answer
24
+ * `{"posts": [...]}`, not a bare array. The SDK unwraps them, and says so
25
+ * on each method.
26
+ * 3. **`PATCH /notifications/:id` is routed and can never succeed.**
27
+ * `Notification` never overrides `updatable_by?`, so `Authorizable`'s
28
+ * default `false` stands and every attempt is `401`. There is no way to
29
+ * mark ONE notification read over HTTP - only
30
+ * {@link NotificationsNamespace.markAllRead}. See that method.
31
+ * 4. **`POST /blog_posts` and `PATCH /blog_posts/:id` are not the same shape
32
+ * of update.** Sending `publish` in a PATCH makes the controller publish
33
+ * and RETURN, silently discarding every other field in the same body. See
34
+ * {@link BlogPostsNamespace.setPublished}.
35
+ * 5. **`money` and `time` on a Space Invaders game arrive as STRINGS.** They
36
+ * are `decimal` columns, and Rails encodes `BigDecimal` as a string so no
37
+ * precision is lost in transit. `kills` next to them is an integer and
38
+ * arrives as a number. See {@link SpaceInvadersGame}.
39
+ * 6. **`GET /services_status` is the expensive one, not `/uptime`.** It pings
40
+ * three external services synchronously, one after another, with no cache.
41
+ * `/uptime` reads a cache. See {@link ServicesStatusNamespace}.
42
+ * 7. **The ids in this file are not one type.** Blogs, posts, notifications,
43
+ * jokes, incidents and Space Invaders games are auto-increment INTEGERS;
44
+ * feedbacks are opaque STRINGS; users inside any of those payloads are
45
+ * strings. There is no rule to remember, only the table.
46
+ * 8. **`POST /feedbacks` is the most heavily capped route here**: 5 per hour
47
+ * per IP, after a bot pushed roughly 200 notification emails through it in
48
+ * one burst, plus a three-minute de-duplication window inside the
49
+ * controller. See {@link FeedbacksNamespace.create}.
50
+ * 9. **"Intel" is two unrelated things sharing a prefix.** `GET /intel/*path`
51
+ * is a generic forwarder to a sidecar whose routes are not in the backend
52
+ * repository, so it stays untyped on purpose ({@link IntelProxyNamespace}).
53
+ * The `/intel_articles`, `/intel_reports`, `/intel_sources`,
54
+ * `/intel_scripts`, `/intel_items`, `/intel_config` and `/intel_stats`
55
+ * routes are ordinary Rails controllers with blueprints, and they are fully
56
+ * typed ({@link IntelNamespace}). Reach for the second set.
57
+ * 10. **Two blog routes are private by accident.** `allow_unauthenticated_access`
58
+ * lists `index` and `show` on both blog controllers and nothing else, so the
59
+ * public PERMALINK (`GET /blogs/:blog/posts/:slug`) rejects anonymous
60
+ * readers while `GET /blog_posts/:id` serves them the same post, and the
61
+ * anonymous email-subscribe branch inside `BlogsController#subscribe` can
62
+ * never run. See {@link BlogPostsNamespace.getBySlugs} and
63
+ * {@link BlogsNamespace.subscribe}.
64
+ *
65
+ * ## No OAuth token reaches ANY of this
66
+ *
67
+ * Not one of the ten controllers declares an `oauth_scope`, and
68
+ * `enforce_oauth_scope!` denies by omission, so a Doorkeeper access token gets
69
+ * `403 {"error":"insufficient_scope"}` on every route in this file - including
70
+ * the ones that are open to callers with NO credential at all
71
+ * (`GET /config`, `GET /jokes`, `GET /blogs`, `/services_status`,
72
+ * `/space_invaders_games/leaderboard`). That is the trap: attaching an OAuth
73
+ * token to a public read turns a working call into a 403. A session (cookie or
74
+ * bearer session token) is the only credential this file accepts; an
75
+ * OAuth-backed integration that wants the public reads must send nothing.
76
+ *
77
+ * ## Rate ceilings
78
+ *
79
+ * Only one route here has a bucket of its own: `POST /feedbacks`, at 5 per
80
+ * hour per IP. Everything else rides the general ceiling - **600 requests per
81
+ * minute** for an authenticated caller, **120 per minute per IP** for an
82
+ * anonymous one. Notably `/services_status/uptime` has NO dedicated bucket
83
+ * even though it was DoSed: it was fixed with a cache and one grouped query
84
+ * rather than a throttle. See {@link ServicesStatusNamespace.uptime}.
85
+ */
86
+ import { ApiClient, Resource } from "../http";
87
+ import type { FileOutput, Id, Json, PageParams, Paginated, QueryParams, QueryValue, RequestOptions, Timestamp } from "../types";
88
+ import type { FsNode } from "./storage";
89
+ /**
90
+ * Filters accepted by the four listings in this file that really are the list
91
+ * DSL. Mirrors the shape the other namespaces use.
92
+ *
93
+ * `search` is a partial, accent-insensitive match; `exact_search` is equality,
94
+ * with an array meaning `IN` and `null` meaning `IS NULL`. Both fail CLOSED:
95
+ * a key the controller did not declare is `400 "Unknown search filter: x"`,
96
+ * never a silently wider result. The declared keys differ per endpoint and are
97
+ * documented on each `list()`; they are much narrower here than you would
98
+ * guess, because most of these controllers declare no `search_params` at all
99
+ * and inherit only `id`, `created_at` and `updated_at`.
100
+ */
101
+ export interface ContentListParams extends PageParams {
102
+ /** Partial, accent-insensitive match. Only the columns the endpoint declares. */
103
+ readonly search?: Record<string, QueryValue>;
104
+ /** Exact match. Array means `IN`, `null` means `IS NULL`. */
105
+ readonly exactSearch?: Record<string, QueryValue>;
106
+ /**
107
+ * Ask for rows in random order. Mutually useful with a small `pageSize`.
108
+ *
109
+ * Two side effects worth knowing: `QueryModifier` applies `RANDOM()` with
110
+ * `reorder`, so it REPLACES any `order` the controller set for itself, and
111
+ * `CrudActions#resources_stale?` short-circuits for a random listing, so the
112
+ * response carries no `ETag` and can never answer `304`.
113
+ */
114
+ readonly random?: boolean;
115
+ }
116
+ /** Primary key of a blog. An INTEGER: `blogs` kept its auto-increment id. */
117
+ export type BlogId = number;
118
+ /** Primary key of a blog post. An integer, like the blog it hangs off. */
119
+ export type BlogPostId = number;
120
+ /** The author of a blog, embedded in {@link Blog}. Not a full user record. */
121
+ export interface BlogAuthor {
122
+ /** User id. A STRING, unlike every other id in this section. */
123
+ readonly id: Id;
124
+ readonly handle: string;
125
+ readonly name: string;
126
+ }
127
+ /**
128
+ * A blog: one per user, created lazily.
129
+ *
130
+ * Deliberately NOT a `BaseRecord`. `BlogBlueprint` inherits `Blueprinter::Base`
131
+ * directly rather than `ApplicationBlueprint`, precisely so the payload keeps
132
+ * the exact key set the web frontend was built against - which means it has
133
+ * `created_at` and NO `updated_at`. Do not reach for one.
134
+ */
135
+ export interface Blog {
136
+ readonly id: BlogId;
137
+ /**
138
+ * URL-safe handle of the blog, and the ONLY way to address it on the read
139
+ * routes. Matches `/\A[a-z0-9][a-z0-9_-]*\z/`, 1-64 characters, unique
140
+ * across the whole table. Defaults to the owner's handle, lowercased.
141
+ */
142
+ readonly slug: string;
143
+ /** Display name. Defaults to `"<name>'s blog"`. Up to 80 characters. */
144
+ readonly name: string;
145
+ /** Up to 240 characters, or `null`. */
146
+ readonly description: string | null;
147
+ /** Who owns it. One blog per user, enforced by a unique index on `user_id`. */
148
+ readonly user: BlogAuthor;
149
+ /**
150
+ * Subscribers with a `confirmed_at`, counted live on every render.
151
+ *
152
+ * In practice that is every subscriber: the only reachable way to subscribe
153
+ * requires a session, and a signed-in subscription is confirmed on the spot.
154
+ * See {@link BlogsNamespace.subscribe}.
155
+ */
156
+ readonly followers_count: number;
157
+ /** Posts with a `published_at`, counted live on every render. */
158
+ readonly published_posts_count: number;
159
+ readonly created_at: Timestamp;
160
+ /**
161
+ * Whether the CALLING user subscribes to this blog. Computed against the
162
+ * caller, so the same row differs per identity - never cache it across
163
+ * identities, and note it is `false` (not `null`) for an anonymous caller.
164
+ */
165
+ readonly is_following: boolean;
166
+ }
167
+ /**
168
+ * A post as it appears in a listing: the summary view.
169
+ *
170
+ * Like {@link Blog}, this is not a `BaseRecord`: the default view of
171
+ * `BlogPostBlueprint` carries neither `created_at` nor `updated_at`. They
172
+ * appear only on {@link BlogPost}, the `:extended` view, which - per the
173
+ * Blueprinter convention - is this shape PLUS extras, never a subset.
174
+ */
175
+ export interface BlogPostSummary {
176
+ readonly id: BlogPostId;
177
+ /** URL-safe, unique within the blog. Derived from the title when omitted. */
178
+ readonly slug: string;
179
+ /** Up to 200 characters. */
180
+ readonly title: string;
181
+ /**
182
+ * Up to 240 characters, derived from the first characters of `content_md`
183
+ * with the markdown punctuation stripped, unless the author wrote one. The
184
+ * derivation runs in a `before_save`, so it is refreshed on every write
185
+ * where the excerpt is blank - and never once it is not.
186
+ */
187
+ readonly excerpt: string | null;
188
+ /** `null` for a draft. Presence of this field IS the published flag. */
189
+ readonly published_at: Timestamp | null;
190
+ /**
191
+ * Estimated reading time, recomputed on every save at 220 words per minute
192
+ * and floored at 1. Server-owned: sending it is ignored.
193
+ */
194
+ readonly reading_minutes: number;
195
+ /** Lowercased, de-duplicated, at most 10. Never `null` in the payload. */
196
+ readonly tags: string[];
197
+ /** The blog it belongs to, trimmed to three fields. */
198
+ readonly blog: {
199
+ readonly id: BlogId;
200
+ readonly slug: string;
201
+ readonly name: string;
202
+ };
203
+ }
204
+ /** A post with its body: the `:extended` view, returned by every single-post route. */
205
+ export interface BlogPost extends BlogPostSummary {
206
+ /**
207
+ * The markdown source, up to 200 000 characters.
208
+ *
209
+ * There is a `content_html` column next to it in the database, rendered on
210
+ * write - but no blueprint exposes it, so the client renders the markdown
211
+ * itself.
212
+ */
213
+ readonly content_md: string | null;
214
+ readonly created_at: Timestamp;
215
+ readonly updated_at: Timestamp;
216
+ /** Whether the CALLING user may edit it. Per-viewer, like `is_following`. */
217
+ readonly is_owner: boolean;
218
+ }
219
+ /** A {@link Blog} plus the posts the caller may see. Returned by `show` and `mine`. */
220
+ export interface BlogWithPosts extends Blog {
221
+ /**
222
+ * Newest first (`published_at DESC, created_at DESC`).
223
+ *
224
+ * NOT paginated and NOT capped: `blog.blog_posts.viewable_by(...).recent`
225
+ * runs with no `limit`, so a blog with a thousand posts returns a thousand
226
+ * summaries in one response. This is the one listing in the file with no
227
+ * ceiling of any kind.
228
+ *
229
+ * On `show` this is published posts only, unless the caller owns the blog,
230
+ * in which case drafts are included too. On `mine` it is every post,
231
+ * published or not.
232
+ */
233
+ readonly posts: BlogPostSummary[];
234
+ }
235
+ /** Fields {@link BlogsNamespace.updateMine} may change. */
236
+ export interface UpdateBlogInput {
237
+ /**
238
+ * New slug. Must match `/\A[a-z0-9][a-z0-9_-]*\z/` (1-64 chars) and be free
239
+ * across the whole table, or the call is `400`. Changing it BREAKS every
240
+ * link already published against the old one: there is no redirect and no
241
+ * history table.
242
+ */
243
+ readonly slug?: string;
244
+ /** Up to 80 characters. */
245
+ readonly name?: string;
246
+ /** Up to 240 characters. */
247
+ readonly description?: string | null;
248
+ }
249
+ /** Result of a subscribe call. */
250
+ export interface BlogSubscribeResult {
251
+ readonly ok: boolean;
252
+ /**
253
+ * `true` when the subscription is live.
254
+ *
255
+ * In practice it is ALWAYS `true` today. The field exists because the
256
+ * controller can also create an unconfirmed, email-only subscription - but
257
+ * that branch is unreachable over HTTP (see
258
+ * {@link BlogsNamespace.subscribe}), so every row this route can actually
259
+ * create is a signed-in one, and `set_confirmed_at_for_user_subs` confirms
260
+ * those on insert.
261
+ *
262
+ * Were a `false` ever to reach you, there would be nothing to do about it:
263
+ * the row carries an `unsubscribe_token`, but no route in the application
264
+ * reads it, so there is no confirmation step to complete.
265
+ */
266
+ readonly confirmed: boolean;
267
+ }
268
+ /**
269
+ * The `blogs` namespace: one blog per user, markdown posts, and a subscriber
270
+ * list.
271
+ *
272
+ * The read routes address a blog by its SLUG, never by its id -
273
+ * `BlogsController#show` does `Blog.find_by(slug: params[:id].downcase)` and
274
+ * nothing else, so passing the numeric id gets `404 "Blog not found"`. The two
275
+ * subscribe routes are the exception: they try the slug first and then fall
276
+ * back to the id, so they accept either.
277
+ *
278
+ * Reads (`GET /blogs`, `GET /blogs/:slug`, `GET /blog_posts`,
279
+ * `GET /blog_posts/:id`, `GET /blogs/:blog/posts/:slug`) are open to anonymous
280
+ * callers; everything else needs a session.
281
+ */
282
+ export declare class BlogsNamespace extends Resource {
283
+ /** Posts, blog metadata and publishing. Also mounted as `oms.blogPosts`. */
284
+ readonly posts: BlogPostsNamespace;
285
+ constructor(http: ApiClient);
286
+ /**
287
+ * `GET /blogs` - the discovery feed: the 30 most recent PUBLISHED posts
288
+ * across every blog on the site.
289
+ *
290
+ * Despite the path this returns POSTS, not blogs, and there is no endpoint
291
+ * anywhere that lists blogs. The wire shape is `{"posts": [...]}`, one of
292
+ * the two envelopes in this file; the array is unwrapped here.
293
+ *
294
+ * Fixed at 30 rows, newest first. No paging, no filters, no `search` - the
295
+ * action is `BlogPost.published.recent.limit(30)` and reads nothing off the
296
+ * query string, so anything you add to it is ignored rather than rejected.
297
+ * To go deeper than 30, there is nothing to page: this is a front page, not
298
+ * an archive.
299
+ *
300
+ * Anonymous-safe, and drafts never leak into it regardless of who asks.
301
+ */
302
+ discover(options?: RequestOptions): Promise<BlogPostSummary[]>;
303
+ /**
304
+ * `GET /blogs/:slug` - one blog with all of its visible posts.
305
+ *
306
+ * The slug is lowercased by the server before the lookup, so case does not
307
+ * matter. A numeric id does NOT work here; use the slug.
308
+ *
309
+ * Published posts only, unless the caller OWNS the blog, in which case their
310
+ * drafts are included. `BlogPost.viewable_by` keys on `blog.user_id` alone,
311
+ * so an admin looking at somebody else's blog sees exactly what the public
312
+ * sees. The `posts` array is unbounded - see {@link BlogWithPosts.posts}.
313
+ *
314
+ * @throws {OmsApiError} 404 `"Blog not found"`.
315
+ */
316
+ show(slug: string, options?: RequestOptions): Promise<BlogWithPosts>;
317
+ /**
318
+ * `GET /blogs/mine` - the caller's own blog, drafts included.
319
+ *
320
+ * **This read has a side effect.** `Blog.find_or_create_for` CREATES the
321
+ * blog row on first call, with the slug defaulted to the caller's handle and
322
+ * the name defaulted to `"<name>'s blog"`, and the creation fires a Discord
323
+ * `blog_created` alert. So "does this user have a blog" is not a question
324
+ * this endpoint can answer - by the time it replies, they do. Call it when
325
+ * the user opens their blog dashboard, not to probe.
326
+ *
327
+ * The default slug is the user's handle, which can collide with a blog
328
+ * somebody already owns under that slug - handles and blog slugs are
329
+ * separate namespaces and nothing keeps them apart. `create!` then raises
330
+ * `ActiveRecord::RecordInvalid`, which reaches the caller as a `422` with a
331
+ * Rails error page rather than this API's usual bare string, and fires a
332
+ * Discord error alert on the way out. Rare, and unfixable from the client:
333
+ * the endpoint takes no arguments.
334
+ *
335
+ * @throws {OmsApiError} 401 `"Session required to access this resource."`.
336
+ * The action also carries its own `unauthorized!("Not authenticated")`
337
+ * guard, but `allow_unauthenticated_access` covers only `index` and `show`,
338
+ * so the framework filter fires first and that message never ships.
339
+ */
340
+ mine(options?: RequestOptions): Promise<BlogWithPosts>;
341
+ /**
342
+ * `PATCH /blogs/mine` - renames or re-slugs the caller's blog.
343
+ *
344
+ * Creates the blog first if there is none, exactly like {@link mine}, so
345
+ * this can be the very first call a client makes.
346
+ *
347
+ * Answers the blog ALONE - no `posts` key, unlike every other blog route.
348
+ * That asymmetry is the reason this returns {@link Blog} and not
349
+ * {@link BlogWithPosts}.
350
+ *
351
+ * Only `slug`, `name` and `description` are permitted; anything else in the
352
+ * body is dropped in silence. Re-slugging breaks published links - see
353
+ * {@link UpdateBlogInput.slug}.
354
+ *
355
+ * @throws {OmsApiError} 400 with the validation sentence when the slug is
356
+ * taken or malformed; 401 `"Session required to access this resource."`.
357
+ */
358
+ updateMine(input: UpdateBlogInput, options?: RequestOptions): Promise<Blog>;
359
+ /**
360
+ * `POST /blogs/:slug/subscribe` - follows a blog.
361
+ *
362
+ * **Signed-in callers only, despite appearances.** The action reads like it
363
+ * supports anonymous email subscriptions - there is a whole branch for it,
364
+ * ending in `400 "Email required for anonymous subscribe"` - but
365
+ * `allow_unauthenticated_access` covers only `index` and `show`, so
366
+ * `require_authentication` rejects an anonymous caller with
367
+ * `401 "Session required to access this resource."` long before that branch
368
+ * runs. The email path is dead code today.
369
+ *
370
+ * Which means `email` is effectively ignored: with a session present the
371
+ * controller always takes the user branch, the subscription is attached to
372
+ * the account, and it is confirmed on insert, so `confirmed` is always
373
+ * `true` and `followers_count` moves. The parameter is kept here because the
374
+ * server accepts it and because the branch could be revived by one line in
375
+ * the controller - not because sending it changes anything today.
376
+ *
377
+ * Idempotent by construction: `find_or_initialize_by` on
378
+ * `(blog, user, email)` means subscribing twice is a no-op that answers
379
+ * `200` both times, so this is one of the few POSTs here where a retry
380
+ * cannot duplicate anything.
381
+ *
382
+ * Accepts either the slug or the numeric id in the path - this route and its
383
+ * `DELETE` twin are the only ones in the file that do.
384
+ *
385
+ * @throws {OmsApiError} 404 `"Blog not found"`; 401 without a session.
386
+ */
387
+ subscribe(slugOrId: string | BlogId, input?: {
388
+ readonly email?: string;
389
+ }, options?: RequestOptions): Promise<BlogSubscribeResult>;
390
+ /**
391
+ * `DELETE /blogs/:slug/subscribe` - unfollows a blog.
392
+ *
393
+ * Signed-in callers only, and it removes only the CALLER's own subscription:
394
+ * the scope is `where(blog:, user: Current.user)`, so an email-only row
395
+ * (were one to exist) could not be removed through here at all.
396
+ *
397
+ * Answers `{"ok": true}` whether or not a subscription existed - it is a
398
+ * `delete_all` on a scope, so "not subscribed" and "unsubscribed" are the
399
+ * same answer, and a double call is harmless.
400
+ *
401
+ * @throws {OmsApiError} 404 `"Blog not found"`; 401
402
+ * `"Session required to access this resource."`.
403
+ */
404
+ unsubscribe(slugOrId: string | BlogId, options?: RequestOptions): Promise<{
405
+ ok: boolean;
406
+ }>;
407
+ }
408
+ /** Arguments for {@link BlogPostsNamespace.create}. */
409
+ export interface CreateBlogPostInput {
410
+ /** Required, up to 200 characters. */
411
+ readonly title: string;
412
+ /**
413
+ * URL-safe slug, unique within the blog. Omit it and the server derives one
414
+ * from the title (lowercased, non-alphanumerics collapsed to `-`, trimmed to
415
+ * 80 characters), falling back to `post-<6 hex>` when the title has no
416
+ * alphanumerics at all - a title written entirely in a non-Latin script
417
+ * therefore gets a random slug, not a transliterated one.
418
+ */
419
+ readonly slug?: string;
420
+ /** Markdown source, up to 200 000 characters. */
421
+ readonly content_md?: string;
422
+ /**
423
+ * Up to 280 characters by the model, but only 240 are ever written by the
424
+ * derivation. Leave it out and the server writes the first 240 characters of
425
+ * the stripped markdown; it is re-derived on every save where it is blank,
426
+ * so clearing it back to `""` re-enables the automatic one.
427
+ */
428
+ readonly excerpt?: string;
429
+ /**
430
+ * Up to 10 tags. Lowercased, trimmed, de-duplicated and blank-filtered by
431
+ * the controller, and anything past the tenth is dropped in silence.
432
+ */
433
+ readonly tags?: readonly string[];
434
+ }
435
+ /**
436
+ * Arguments for {@link BlogPostsNamespace.update}.
437
+ *
438
+ * `tags` is optional in the type and DANGEROUS to omit. `post_params` ends
439
+ * with an unconditional `raw[:tags] = (raw[:tags] || []) ...`, so a PATCH that
440
+ * does not mention tags assigns the EMPTY ARRAY over whatever the post had.
441
+ * There is no partial-update semantics for this field: resend the tags you
442
+ * already hold on every update, or watch them disappear. Same shape of bug as
443
+ * `manifest_json` on movie addons.
444
+ */
445
+ export interface UpdateBlogPostInput {
446
+ readonly title?: string;
447
+ /** Changing it breaks published links; uniqueness is scoped to the blog. */
448
+ readonly slug?: string;
449
+ readonly content_md?: string;
450
+ readonly excerpt?: string;
451
+ /** ALWAYS send this. Omitting it clears the post's tags. See the interface docs. */
452
+ readonly tags?: readonly string[];
453
+ }
454
+ /**
455
+ * Posts, reachable as `oms.content.blogs.posts`.
456
+ *
457
+ * A post lives under exactly one blog and a user has exactly one blog, so
458
+ * there is no "which blog" argument anywhere: {@link create} always writes to
459
+ * the caller's own, creating it if needed.
460
+ */
461
+ export declare class BlogPostsNamespace extends Resource {
462
+ /**
463
+ * `GET /blog_posts` - the 50 most recent posts the caller may see, newest
464
+ * first, optionally narrowed to one blog.
465
+ *
466
+ * Envelope: the wire shape is `{"posts": [...]}` and the array is unwrapped
467
+ * here.
468
+ *
469
+ * NOT the list DSL, despite the plural path. `blog_slug` is the only
470
+ * parameter the action reads; `search`, `exact_search` and `modifiers` are
471
+ * ignored rather than rejected, and the limit of 50 is not negotiable. There
472
+ * is no way to page past it, so this is a feed and not an archive - to walk
473
+ * a whole blog, read {@link BlogsNamespace.show}, whose `posts` array is
474
+ * uncapped.
475
+ *
476
+ * Visibility follows the caller: published posts always, plus the caller's
477
+ * OWN drafts. An anonymous caller sees published posts only.
478
+ *
479
+ * @param input.blogSlug Restrict to one blog. An unknown slug does NOT 404 -
480
+ * `find_by` returns nil and the action silently falls back to the
481
+ * site-wide listing, so a typo here returns everybody's posts instead of
482
+ * an empty list. Check the `blog` on each row if that distinction matters.
483
+ */
484
+ list(input?: {
485
+ readonly blogSlug?: string;
486
+ }, options?: RequestOptions): Promise<BlogPostSummary[]>;
487
+ /**
488
+ * `GET /blog_posts/:id` - one post by numeric id, with its body.
489
+ *
490
+ * @throws {OmsApiError} 404 `"Post not found"`; **401**
491
+ * `"Draft only visible to author"` when the post exists but is
492
+ * unpublished and the caller is not its author. Note that this is a 401
493
+ * rather than a 404, so it confirms that a draft with that id exists.
494
+ */
495
+ get(id: BlogPostId, options?: RequestOptions): Promise<BlogPost>;
496
+ /**
497
+ * `GET /blogs/:blogSlug/posts/:slug` - one post by the pair of slugs, which
498
+ * is the shape a public permalink has.
499
+ *
500
+ * **This route needs a session, and the id route does not.** That is almost
501
+ * certainly a mistake in the backend and it is worth knowing before you
502
+ * build a public permalink on it: `BlogPostsController` declares
503
+ * `allow_unauthenticated_access only: %i[index show]`, and `show_by_slugs`
504
+ * is a third action that was never added to the list. So an anonymous
505
+ * reader following a shared link gets `401 "Session required to access this
506
+ * resource."` here, while {@link get} hands them the very same published
507
+ * post. Until that is fixed, render public permalinks through {@link get}
508
+ * with the numeric id, or expect signed-in readers only.
509
+ *
510
+ * Both slugs are lowercased server-side before the lookup. The route is
511
+ * declared with `constraints: { blog_slug: /[^\/]+/, slug: /[^\/]+/ }`, so a
512
+ * slug containing a slash cannot reach it at all - not a concern for
513
+ * server-minted slugs, which are `[a-z0-9_-]` only.
514
+ *
515
+ * @throws {OmsApiError} 401 without a session, before anything else is
516
+ * checked; 404 `"Blog not found"` or `"Post not found"`; 401
517
+ * `"Draft only visible to author"`.
518
+ */
519
+ getBySlugs(blogSlug: string, slug: string, options?: RequestOptions): Promise<BlogPost>;
520
+ /**
521
+ * `POST /blog_posts` - writes a new post to the caller's own blog. `201`.
522
+ *
523
+ * There is no blog argument because there is no choice: the controller calls
524
+ * `Blog.find_or_create_for(Current.user)`, so this CREATES the caller's blog
525
+ * as a side effect on their very first post, exactly like
526
+ * {@link BlogsNamespace.mine}.
527
+ *
528
+ * The post starts as a DRAFT - `published_at` is not settable here and no
529
+ * amount of arguments will publish it. Publishing is a second call, and it
530
+ * is {@link setPublished}, not {@link update}.
531
+ *
532
+ * Rides the general ceiling: there is no per-user cap on how many posts may
533
+ * be created, and no length cap beyond the model's 200 000 characters of
534
+ * markdown.
535
+ *
536
+ * @throws {OmsApiError} 401 `"Session required to access this resource."`;
537
+ * 400 with the validation sentence, most often the slug already existing
538
+ * in this blog.
539
+ */
540
+ create(input: CreateBlogPostInput, options?: RequestOptions): Promise<BlogPost>;
541
+ /**
542
+ * `PATCH /blog_posts/:id` - edits a post's fields.
543
+ *
544
+ * Do NOT put `publish` in this body. The controller checks for it FIRST and,
545
+ * when it is a boolean or the string `"true"`/`"false"`, publishes or
546
+ * unpublishes and returns immediately - `ok!` raises the response - so every
547
+ * other field in the same request is discarded without a word. That is a
548
+ * silent data loss, not an error you can catch. Use {@link setPublished} for
549
+ * the flag and this method for the content; two calls, in either order.
550
+ *
551
+ * Always send `tags`, including when they have not changed. See
552
+ * {@link UpdateBlogPostInput}.
553
+ *
554
+ * Editing does not change `published_at`, so an edit to a published post
555
+ * stays published and does not move in the feed's ordering.
556
+ *
557
+ * @throws {OmsApiError} 404 `"Post not found"`; 401 `"Not your post"`; 400
558
+ * with the validation sentence.
559
+ */
560
+ update(id: BlogPostId, input: UpdateBlogPostInput, options?: RequestOptions): Promise<BlogPost>;
561
+ /**
562
+ * `PATCH /blog_posts/:id` with `{ publish }` - the publish switch, on its
563
+ * own.
564
+ *
565
+ * Separated from {@link update} because the controller treats it as an
566
+ * early-return branch rather than as a field: a body carrying `publish`
567
+ * never reaches `post.update(post_params)`, so mixing the two loses the
568
+ * content edit. Sending it alone is the only safe way to use it.
569
+ *
570
+ * `publish: true` stamps `published_at` with the current time - and moves
571
+ * the post to the top of every `recent` ordering. Re-publishing an already
572
+ * published post is a no-op that still answers `200` with the ORIGINAL
573
+ * `published_at`, so this cannot be used to bump a post. `publish: false`
574
+ * clears `published_at`, which unlists the post everywhere and, for a
575
+ * non-owner, turns {@link get} into a 401.
576
+ *
577
+ * @throws {OmsApiError} 404 `"Post not found"`; 401 `"Not your post"`.
578
+ */
579
+ setPublished(id: BlogPostId, publish: boolean, options?: RequestOptions): Promise<BlogPost>;
580
+ /**
581
+ * `DELETE /blog_posts/:id` - permanent. `204`, no body.
582
+ *
583
+ * Takes the attached `cover_image` with it (`dependent: :destroy`). There is
584
+ * no trash and no undo.
585
+ *
586
+ * @throws {OmsApiError} 404 `"Post not found"`; 401 `"Not your post"`.
587
+ */
588
+ destroy(id: BlogPostId, options?: RequestOptions): Promise<void>;
589
+ }
590
+ /** Primary key of a notification. An INTEGER. */
591
+ export type NotificationId = number;
592
+ /**
593
+ * The `kind` strings the backend emits today.
594
+ *
595
+ * NOT a closed set and not validated anywhere - `Notification` only requires
596
+ * `kind` to be present, so a new feature can add one without a migration. The
597
+ * union is here so the kinds you handle autocomplete; keep a default branch
598
+ * for the ones you do not, and never let an unknown kind break the inbox.
599
+ *
600
+ * Each kind implies a different {@link Notification.context} shape, which is
601
+ * why `context` is typed as an open record rather than a discriminated union:
602
+ * the backend guarantees a JSON object and nothing about its keys.
603
+ */
604
+ export type NotificationKind = "friendship_request" | "friendship_accepted" | "user_followed" | "message_received" | "fs_grant_received" | "jam_invite" | "vocal_separation_done" | "vocal_separation_failed" | (string & {});
605
+ /**
606
+ * One notification in a user's inbox.
607
+ *
608
+ * Unlike most of this file it IS a full `ApplicationBlueprint` record, so it
609
+ * carries `id`, `created_at` and `updated_at`. The `:extended` view adds
610
+ * nothing, so a notification arriving over the cable and one arriving over
611
+ * HTTP have the same fields.
612
+ */
613
+ export interface Notification {
614
+ /** An integer. The web frontend types it as a string; it is a JSON number. */
615
+ readonly id: NotificationId;
616
+ /** What happened. See {@link NotificationKind}. */
617
+ readonly kind: NotificationKind;
618
+ /**
619
+ * Free-form JSON payload, whose keys depend entirely on `kind` - these are
620
+ * the i18n interpolation values the client renders the sentence with.
621
+ *
622
+ * Never `null` (the column is `NOT NULL DEFAULT '{}'`), and never large: the
623
+ * emitter runs user-supplied text through a 120-character preview before
624
+ * storing it, so a message-received notification carries a truncated
625
+ * snippet, not the message.
626
+ *
627
+ * One shape is documented outside the code and worth having here:
628
+ * `jam_invite` carries `{ jam_id, host_id, host_handle, inviter_id,
629
+ * inviter_handle }`. The rest you learn by reading a row.
630
+ */
631
+ readonly context: Record<string, unknown>;
632
+ /** Whether it has been marked read. See {@link NotificationsNamespace.markAllRead}. */
633
+ readonly read: boolean;
634
+ /** Owner. Always the caller: the scope is `user.notifications`. A STRING. */
635
+ readonly user_id: Id;
636
+ readonly created_at: Timestamp;
637
+ readonly updated_at: Timestamp;
638
+ }
639
+ /**
640
+ * The `notifications` namespace: the per-user inbox.
641
+ *
642
+ * ## This covers the HTTP half only
643
+ *
644
+ * Notifications are pushed in real time over ActionCable, on the per-user
645
+ * `NotificationsChannel` stream, and that is where a live inbox gets its
646
+ * updates from: the channel transmits `{type: "unread_count", unread_count}`
647
+ * on subscribe, `{type: "created", notification, unread_count}` when one
648
+ * arrives, and `{type: "unread_count", unread_count}` again whenever the read
649
+ * state or the row count changes. The SDK does not open that socket and does
650
+ * not wrap it - it has no cable client - so these methods are the polling
651
+ * fallback and the write path, not the way to keep a badge live. A host with a
652
+ * socket should subscribe and use {@link unreadCount} only for the first
653
+ * paint.
654
+ *
655
+ * ## You cannot mark ONE notification read
656
+ *
657
+ * `PATCH /notifications/:id` is routed, and it cannot succeed for anybody.
658
+ * `Notification` overrides `destroyable_by?` but never `updatable_by?`, so
659
+ * `Authorizable`'s default `false` stands and `CrudActions#update` answers
660
+ * `401 "You are not authorized to update this resource"` on every call - for
661
+ * the owner, for an admin, for everyone. The model even has an
662
+ * `after_update_commit` hook waiting to broadcast the new count, which is dead
663
+ * code today. The SDK therefore exposes no `markRead(id)`: there is nothing
664
+ * honest to put behind it. Mark the whole inbox with {@link markAllRead}, or
665
+ * remove the row with {@link dismiss}, which is what the web client does.
666
+ *
667
+ * There is also no `GET /notifications/:id`: the resource is declared
668
+ * `only: [:index, :update, :destroy]`, so a single fetch by id is a routing
669
+ * 404. Read one out of {@link list}.
670
+ *
671
+ * Everything here needs a session and rides the general 600/min ceiling.
672
+ */
673
+ export declare class NotificationsNamespace extends Resource {
674
+ /**
675
+ * `GET /notifications` - the caller's inbox, one page at a time.
676
+ *
677
+ * Scoped to the caller by `viewable_by` (`user.notifications`), so there is
678
+ * no way to read anybody else's and the `user_id` filter below is redundant.
679
+ *
680
+ * **The filterable columns are almost none.** The controller declares
681
+ * `search_params :user_id`, which the DSL merges with the three defaults, so
682
+ * the complete allowlist is `id`, `created_at`, `updated_at` and `user_id`.
683
+ * `read` and `kind` are NOT on it, and filters fail closed: asking for
684
+ * `exact_search: { read: false }` - the obvious way to fetch the unread ones -
685
+ * is `400 "Unknown exact_search filter: read"`, not an unfiltered list.
686
+ * Fetch a page and filter client-side, or read {@link unreadCount} for the
687
+ * badge.
688
+ *
689
+ * No default ordering is declared, so rows come back in whatever order
690
+ * Postgres chooses. Pass `order: "created_at:desc"` for an inbox; there is
691
+ * an index on `(user_id, created_at)` behind it.
692
+ *
693
+ * Sends an `ETag`, so an unchanged page answers `304` and costs nothing -
694
+ * except with `random: true`, which disables the check.
695
+ */
696
+ list(params?: ContentListParams, options?: RequestOptions): Promise<Paginated<Notification>>;
697
+ /**
698
+ * `GET /notifications/unread_count` - how many unread notifications the
699
+ * caller has. Unwraps the `{"count": n}` the server sends.
700
+ *
701
+ * One indexed `COUNT` behind a partial index (`WHERE read = false`), so it is
702
+ * cheap - but it is still a request per call, and the cable already pushes
703
+ * this number on subscribe and on every change. Poll it only where there is
704
+ * no socket.
705
+ */
706
+ unreadCount(options?: RequestOptions): Promise<number>;
707
+ /**
708
+ * `POST /notifications/read_all` - marks every unread notification read.
709
+ *
710
+ * Returns how many rows changed, which is the unread count from an instant
711
+ * ago; calling it twice returns `0` the second time. `200`, not `201` - it
712
+ * creates nothing.
713
+ *
714
+ * Runs as a single `update_all`, so no model callback fires and the per-row
715
+ * broadcast is skipped; the controller pushes the new count over the cable
716
+ * by hand afterwards, which is why every device still updates.
717
+ *
718
+ * Idempotent, so a retry is harmless. It is not enabled by default (the
719
+ * transport does not replay a `POST`); pass `retry: {}` if you want one.
720
+ */
721
+ markAllRead(options?: RequestOptions): Promise<number>;
722
+ /**
723
+ * `DELETE /notifications/:id` - removes one notification. `204`, no body.
724
+ *
725
+ * This is the closest thing to "mark as read" the API has, and it is what
726
+ * the web client uses: the row is gone, so the unread count drops and an
727
+ * `after_destroy_commit` pushes the new count over the cable.
728
+ *
729
+ * Owner only - `viewable_by` scopes the lookup to the caller, so somebody
730
+ * else's id is `404 "Resource not found"` rather than a 401.
731
+ */
732
+ dismiss(id: NotificationId, options?: RequestOptions): Promise<void>;
733
+ }
734
+ /**
735
+ * Primary key of a feedback report. A STRING, not an integer: `feedbacks` is
736
+ * one of the tables that moved to opaque random ids, and it is the only one in
737
+ * this file that did.
738
+ */
739
+ export type FeedbackId = Id;
740
+ /** Triage state of a report. Mirrors `Feedback::STATUSES`. */
741
+ export declare const FEEDBACK_STATUSES: readonly ["new", "read", "archived"];
742
+ /** One of {@link FEEDBACK_STATUSES}. */
743
+ export type FeedbackStatus = (typeof FEEDBACK_STATUSES)[number];
744
+ /** Longest report the endpoint accepts, in BYTES. Mirrors `Feedback::CONTENT_MAX_LENGTH`. */
745
+ export declare const FEEDBACK_CONTENT_MAX_BYTES = 5000;
746
+ /** How many attachments survive one submission. Mirrors `MAX_ATTACHMENTS_COUNT`. */
747
+ export declare const FEEDBACK_MAX_ATTACHMENTS = 6;
748
+ /** Combined decoded size of the attachments that get stored. 10 MiB. */
749
+ export declare const FEEDBACK_MAX_ATTACHMENTS_TOTAL_BYTES: number;
750
+ /** Longest single `data:` URL the attacher will decode. 15 MiB of base64. */
751
+ export declare const FEEDBACK_MAX_ATTACHMENT_DATA_URL_BYTES: number;
752
+ /** Anonymous submissions allowed per hour per IP, before rack-attack answers 429. */
753
+ export declare const FEEDBACK_CREATE_RATE_LIMIT_PER_HOUR = 5;
754
+ /** How long an identical report from the same IP is folded into the first one. */
755
+ export declare const FEEDBACK_DUPLICATE_WINDOW_MS: number;
756
+ /** A stored attachment, as it appears on a report. Admin-visible only. */
757
+ export interface FeedbackAttachment {
758
+ /** ActiveStorage blob id. An INTEGER, and the segment `attachmentUrl` needs. */
759
+ readonly blob_id: number;
760
+ readonly filename: string;
761
+ readonly content_type: string;
762
+ readonly byte_size: number;
763
+ }
764
+ /** The submitter, when they were signed in. Carries their email, so admin-only. */
765
+ export interface FeedbackSubmitter {
766
+ readonly id: Id;
767
+ readonly handle: string;
768
+ readonly name: string;
769
+ readonly email: string;
770
+ }
771
+ /**
772
+ * A feedback report, as an admin reads it.
773
+ *
774
+ * Nobody else ever sees this shape: `viewable_by` is `user&.admin? ? all : none`,
775
+ * so a non-admin's listing is empty and a non-admin's `show` is a 404. The
776
+ * submitter cannot read back what they sent - {@link FeedbacksNamespace.create}
777
+ * answers with an id and nothing else.
778
+ */
779
+ export interface Feedback {
780
+ readonly id: FeedbackId;
781
+ /** What the person wrote. Up to {@link FEEDBACK_CONTENT_MAX_BYTES} bytes. */
782
+ readonly content: string;
783
+ readonly status: FeedbackStatus;
784
+ /**
785
+ * The three context keys the controller keeps (`path`, `source`,
786
+ * `user_agent`); everything else the client sent is dropped before the row
787
+ * is written. `{}` when nothing was sent.
788
+ */
789
+ readonly context: Record<string, string>;
790
+ /** The account that submitted it, or `null` for an anonymous report. */
791
+ readonly user_id: Id | null;
792
+ /** Reply address for an anonymous report, or `null`. */
793
+ readonly email: string | null;
794
+ /**
795
+ * ISO country resolved from the submitter's IP by `FeedbackIntakeJob`.
796
+ *
797
+ * Written by a background job AFTER the response, so it is `null` on a row
798
+ * read immediately after submission and fills in a moment later. Same for
799
+ * {@link device_name}.
800
+ */
801
+ readonly country: string | null;
802
+ /** Device name parsed out of the user agent, by the same background job. */
803
+ readonly device_name: string | null;
804
+ /** Expanded account, or `null` when the report was anonymous. */
805
+ readonly user: FeedbackSubmitter | null;
806
+ /** Screenshots, in submission order. Empty when none survived the filters. */
807
+ readonly attachments: FeedbackAttachment[];
808
+ readonly created_at: Timestamp;
809
+ readonly updated_at: Timestamp;
810
+ }
811
+ /** One screenshot, sent inline as a `data:` URL rather than as multipart. */
812
+ export interface FeedbackAttachmentInput {
813
+ /**
814
+ * A full `data:<mime>;base64,<payload>` URL. Anything that does not match
815
+ * that exact regex - a bare base64 string, a `data:` URL that is not base64 -
816
+ * is skipped in silence.
817
+ */
818
+ readonly data_url: string;
819
+ /** Name to store. Sanitised server-side; defaults to `feedback-<id>-attachment-<n>.<ext>`. */
820
+ readonly filename?: string;
821
+ }
822
+ /** Arguments for {@link FeedbacksNamespace.create}. */
823
+ export interface CreateFeedbackInput {
824
+ /**
825
+ * The report. Trimmed, and rejected when blank
826
+ * (`400 "Feedback can't be empty"`) or over
827
+ * {@link FEEDBACK_CONTENT_MAX_BYTES} BYTES - bytes, not characters, so
828
+ * accented text runs out sooner than the number suggests
829
+ * (`400 "Feedback is too long"`).
830
+ */
831
+ readonly content: string;
832
+ /**
833
+ * Reply address. Only meaningful for an anonymous report: a signed-in
834
+ * submitter is linked by `user_id` and their account email is what the admin
835
+ * sees. Validated against `URI::MailTo::EMAIL_REGEXP` when present.
836
+ */
837
+ readonly email?: string;
838
+ /**
839
+ * Where the report came from. Only `path`, `source` and `user_agent`
840
+ * survive; every other key is dropped without an error.
841
+ */
842
+ readonly context?: {
843
+ readonly path?: string;
844
+ readonly source?: string;
845
+ readonly user_agent?: string;
846
+ };
847
+ /**
848
+ * Screenshots, at most {@link FEEDBACK_MAX_ATTACHMENTS}.
849
+ *
850
+ * Every rule here fails SILENTLY - the attacher logs and moves on, and the
851
+ * submission still answers `201`. An attachment is dropped when it is not a
852
+ * base64 `data:` URL, when its MIME type is not `image/*`, when the URL is
853
+ * over {@link FEEDBACK_MAX_ATTACHMENT_DATA_URL_BYTES}, or when the running
854
+ * decoded total passes {@link FEEDBACK_MAX_ATTACHMENTS_TOTAL_BYTES} (which
855
+ * drops that one AND every one after it). Anything past the sixth is
856
+ * discarded before the loop even starts. So do not treat a `201` as proof
857
+ * the screenshots arrived; only an admin reading {@link Feedback.attachments}
858
+ * can confirm that.
859
+ *
860
+ * Base64 is roughly 4/3 the size of the bytes, and the whole thing travels
861
+ * inside one JSON body: production sits behind Cloudflare's ~100 MB request
862
+ * cap, which rejects an oversized body with a `413` of its own before Rails
863
+ * sees it.
864
+ */
865
+ readonly attachments?: readonly FeedbackAttachmentInput[];
866
+ /**
867
+ * Cloudflare Turnstile token. REQUIRED for an anonymous submission and
868
+ * ignored for a signed-in one.
869
+ *
870
+ * Get the site key from {@link SiteConfigNamespace.get} first. Missing is
871
+ * `400 "Captcha token missing"`; present but not verifying is
872
+ * `403 "Captcha verification failed"`. A token is single-use at Cloudflare,
873
+ * so it cannot be replayed - which also means an SDK-level retry of a failed
874
+ * anonymous submission needs a FRESH token, not the same one.
875
+ */
876
+ readonly cf_turnstile_token?: string;
877
+ }
878
+ /**
879
+ * The `feedbacks` namespace: the site's feedback box, plus its admin queue.
880
+ *
881
+ * Two audiences and one route table. {@link create} is the only thing a normal
882
+ * caller can reach, and it is deliberately anonymous-friendly; everything else
883
+ * is `before_action :require_admin!` and answers `401` with a `null` body to
884
+ * anyone else.
885
+ */
886
+ export declare class FeedbacksNamespace extends Resource {
887
+ /**
888
+ * `POST /feedbacks` - submits a report. `201` with `{"id": "..."}`, which is
889
+ * unwrapped here to the id string.
890
+ *
891
+ * The response carries the id ALONE. There is no way to read the row back
892
+ * without being an admin, so the id is only useful for correlating with a
893
+ * support conversation.
894
+ *
895
+ * ## The ceilings, and why they are there
896
+ *
897
+ * This route has the only dedicated rack-attack bucket in this file:
898
+ * **{@link FEEDBACK_CREATE_RATE_LIMIT_PER_HOUR} per hour, keyed on the IP**,
899
+ * added after a bot pushed roughly 200 admin notification emails through it
900
+ * in a single burst. It is keyed on the IP for EVERY caller, signed in or
901
+ * not, so a shared egress address (an office, a mobile carrier's NAT, a
902
+ * corporate VPN) shares the budget. Over it, `429` with
903
+ * `{"error":"rate_limited"}`, which arrives here as an {@link OmsQuotaError}.
904
+ *
905
+ * On top of that the controller de-duplicates: the same `content` from the
906
+ * same IP inside {@link FEEDBACK_DUPLICATE_WINDOW_MS} returns the id of the
907
+ * EXISTING row with a `201` and writes nothing, attaches nothing and sends
908
+ * no email. So a double-submitted form is harmless, and a retry inside the
909
+ * window is genuinely idempotent - but note the flip side: a user who
910
+ * legitimately sends the same short sentence twice in three minutes gets one
911
+ * report, and the second submission's ATTACHMENTS are silently lost, because
912
+ * the de-duplication branch returns before the attacher runs.
913
+ *
914
+ * ## What happens after the 201
915
+ *
916
+ * `FeedbackIntakeJob` runs on the queue: geo-locates the IP into
917
+ * {@link Feedback.country}, parses the user agent into
918
+ * {@link Feedback.device_name}, sends one coalesced email to every admin,
919
+ * and fires a Discord alert. None of it blocks the response, and none of it
920
+ * can fail the submission.
921
+ *
922
+ * The submitter's IP and user agent are stored on the row regardless of
923
+ * whether they signed in. Say so in your UI if that matters.
924
+ *
925
+ * @throws {OmsApiError} 400 `"Feedback can't be empty"` / `"Feedback is too long"`
926
+ * / `"Captcha token missing"`; 403 `"Captcha verification failed"`.
927
+ * @throws {OmsQuotaError} 429 once the per-IP hourly budget is spent.
928
+ */
929
+ create(input: CreateFeedbackInput, options?: RequestOptions): Promise<FeedbackId>;
930
+ /**
931
+ * `GET /feedbacks` - the admin triage queue. **Admin only.**
932
+ *
933
+ * A non-admin is stopped by `before_action :require_admin!` with a `401`
934
+ * whose body is `null` - no message to show the user, so write your own.
935
+ * `Feedback.viewable_by` collapsing to `none` for a non-admin is the second
936
+ * layer behind that, not the one you will hit.
937
+ *
938
+ * Filterable on `status` and `user_id`, plus the three defaults (`id`,
939
+ * `created_at`, `updated_at`). Any other key is `400`.
940
+ *
941
+ * The scope is ordered `created_at DESC` before the DSL runs, and
942
+ * `modifiers[order]` uses `reorder`, so passing {@link ContentListParams.order}
943
+ * REPLACES that default rather than refining it.
944
+ */
945
+ list(params?: ContentListParams, options?: RequestOptions): Promise<Paginated<Feedback>>;
946
+ /**
947
+ * `GET /feedbacks/:id` - one report in full. **Admin only**; anybody else
948
+ * gets `401` with a `null` body.
949
+ */
950
+ get(id: FeedbackId, options?: RequestOptions): Promise<Feedback>;
951
+ /**
952
+ * `PATCH /feedbacks/:id` - moves a report through triage. **Admin only.**
953
+ *
954
+ * `status` is the only writable field: `update_params :status` is the whole
955
+ * allowlist, so `content` and `email` cannot be edited, and a value outside
956
+ * {@link FEEDBACK_STATUSES} is rejected by a `before_update` hook with
957
+ * `400 "Invalid status"` before the model is touched.
958
+ */
959
+ setStatus(id: FeedbackId, status: FeedbackStatus, options?: RequestOptions): Promise<Feedback>;
960
+ /**
961
+ * `DELETE /feedbacks/:id` - permanent, attachments included. `204`.
962
+ * **Admin only.**
963
+ */
964
+ destroy(id: FeedbackId, options?: RequestOptions): Promise<void>;
965
+ /**
966
+ * `GET /feedbacks/:id/attachment/:blobId` - downloads one screenshot.
967
+ * **Admin only.**
968
+ *
969
+ * The endpoint answers a `302` into object storage, not the bytes, so this
970
+ * follows the redirect and buffers the result. That works in the CLI and in
971
+ * React Native; in a BROWSER it is the same CORS trap `account.picture`
972
+ * documents - the redirect target does not accept a credentialed
973
+ * cross-origin request, and the fetch fails after the 302. A web client
974
+ * should point an `<img>` at {@link attachmentUrl} instead and let the
975
+ * browser follow the redirect without credentials.
976
+ */
977
+ attachment(id: FeedbackId, blobId: number, options?: RequestOptions): Promise<FileOutput>;
978
+ /**
979
+ * The absolute URL of an attachment, for an `<img src>` or an `<a href>`.
980
+ *
981
+ * Builds the string and makes no request, so it carries whatever credential
982
+ * the BROWSER attaches - which for a cookie session on the API's own origin
983
+ * is the session cookie, and for a bearer-token client is nothing at all. A
984
+ * token-authenticated host has to fetch the bytes with {@link attachment}
985
+ * instead; there is no query-string credential this SDK will mint for you.
986
+ */
987
+ attachmentUrl(id: FeedbackId, blobId: number): string;
988
+ }
989
+ /** Primary key of a joke. An INTEGER. */
990
+ export type JokeId = number;
991
+ /**
992
+ * A joke.
993
+ *
994
+ * `JokeBlueprint` extends `ApplicationBlueprint`, so unlike the blog records
995
+ * this one really does carry all three base fields.
996
+ */
997
+ export interface Joke {
998
+ readonly id: JokeId;
999
+ /**
1000
+ * Language tag, as whoever typed it wrote it. Free text with a presence
1001
+ * validation and NOTHING else - no inclusion list, no normalisation - so the
1002
+ * table can and does hold `"pt"` next to `"PT"` next to `"pt-PT"`. Compare
1003
+ * case-insensitively, and see {@link JokesNamespace.list} for why you cannot
1004
+ * make the server do the filtering.
1005
+ */
1006
+ readonly lang: string;
1007
+ /**
1008
+ * The joke. A `varchar` with no database limit and no model validation; the
1009
+ * web composer caps input at 255 characters as a house rule, which nothing
1010
+ * server-side enforces.
1011
+ */
1012
+ readonly content: string;
1013
+ readonly created_at: Timestamp;
1014
+ readonly updated_at: Timestamp;
1015
+ }
1016
+ /** Arguments for {@link JokesNamespace.create}. Both fields are required by the model. */
1017
+ export interface JokeInput {
1018
+ readonly lang: string;
1019
+ readonly content: string;
1020
+ }
1021
+ /**
1022
+ * The `jokes` namespace: the joke table behind the site's loading screens.
1023
+ *
1024
+ * Reading is fully public; writing is admin-only. `Joke.viewable_by` is `all`,
1025
+ * so every joke is visible to every caller including anonymous ones, and
1026
+ * `creatable_by?`/`updatable_by?`/`destroyable_by?` all reduce to
1027
+ * `user.admin?`.
1028
+ */
1029
+ export declare class JokesNamespace extends Resource {
1030
+ /**
1031
+ * `GET /jokes` - the joke table, paged. Anonymous callers welcome.
1032
+ *
1033
+ * **You cannot filter by language.** `JokesController` declares no
1034
+ * `search_params`, so the allowlist is only the three defaults - `id`,
1035
+ * `created_at`, `updated_at` - and `lang` is not on it. `search: { lang: "pt" }`
1036
+ * is `400 "Unknown search filter: lang"`, not a wider result: the DSL fails
1037
+ * closed. Pull a page and filter client-side, which is what every caller
1038
+ * ends up doing.
1039
+ *
1040
+ * For "give me a joke", `random: true` with `pageSize: 1` is the whole
1041
+ * recipe: `QueryModifier` applies `ORDER BY RANDOM()` and the pagination is
1042
+ * applied after it. Note that a random listing carries no `ETag` and can
1043
+ * never answer `304`, which is exactly what you want here and exactly what
1044
+ * you do not want on a normal page.
1045
+ *
1046
+ * `modifiers[order]` also accepts a third segment for an explicit value
1047
+ * ordering (`"lang:asc:pt,en"` puts those languages first), which the rest
1048
+ * of the SDK does not advertise because almost nothing needs it.
1049
+ */
1050
+ list(params?: ContentListParams, options?: RequestOptions): Promise<Paginated<Joke>>;
1051
+ /**
1052
+ * `POST /jokes` - adds a joke. `201`. **Admin only.**
1053
+ *
1054
+ * A signed-in non-admin gets `401 "You are not authorized to create this
1055
+ * resource"`; an anonymous caller gets `401 "Session required to access
1056
+ * this resource."` from the authentication filter first.
1057
+ */
1058
+ create(input: JokeInput, options?: RequestOptions): Promise<Joke>;
1059
+ /**
1060
+ * `PATCH /jokes/:id` - edits a joke. **Admin only.**
1061
+ *
1062
+ * Both fields are permitted and both are optional; the model requires each
1063
+ * to be present, so sending `content: ""` is `400`, not a clear.
1064
+ */
1065
+ update(id: JokeId, input: Partial<JokeInput>, options?: RequestOptions): Promise<Joke>;
1066
+ /**
1067
+ * `DELETE /jokes/:id` - removes a joke. `204`. **Admin only.**
1068
+ *
1069
+ * There is no `GET /jokes/:id`: the resource is declared
1070
+ * `only: [:create, :index, :update, :destroy]`, so a single fetch by id is a
1071
+ * routing 404.
1072
+ */
1073
+ destroy(id: JokeId, options?: RequestOptions): Promise<void>;
1074
+ }
1075
+ /**
1076
+ * The public configuration blob. One key today; treat it as open, since this
1077
+ * is where any future "the browser needs to know this before signing in"
1078
+ * value will land.
1079
+ */
1080
+ export interface SiteConfig {
1081
+ /**
1082
+ * Cloudflare Turnstile site key, for rendering the widget.
1083
+ *
1084
+ * `null` when the credential is not configured - in development, and in any
1085
+ * environment where the key was never set. A `null` here does NOT mean the
1086
+ * captcha is disabled server-side: `require_captcha_if_anonymous!` still
1087
+ * runs and still rejects an anonymous {@link FeedbacksNamespace.create}, so
1088
+ * a client that skips the widget because this was null will see a 400 it
1089
+ * cannot explain. Treat `null` as "anonymous submission is unavailable".
1090
+ */
1091
+ readonly turnstile_site_key: string | null;
1092
+ }
1093
+ /**
1094
+ * The `config` namespace: one anonymous GET that bootstraps the client.
1095
+ *
1096
+ * Deliberately tiny and deliberately public - it is the only thing a client
1097
+ * can read before it has any credential at all, and the only reason it exists
1098
+ * is that the Turnstile widget needs a site key before the anonymous feedback
1099
+ * form can be submitted.
1100
+ */
1101
+ export declare class SiteConfigNamespace extends Resource {
1102
+ /**
1103
+ * `GET /config` - the public configuration blob.
1104
+ *
1105
+ * Anonymous, no side effects, reads no database. It is not cached
1106
+ * server-side and has no `ETag`, so it is a full round trip every time -
1107
+ * fetch it once at boot and hold it, do not call it per form.
1108
+ *
1109
+ * Counts against the general ceiling like everything else (120/min per IP
1110
+ * anonymous), and - the trap this file repeats - answers `403` if you attach
1111
+ * an OAuth access token to it, because no controller here declares a scope.
1112
+ */
1113
+ get(options?: RequestOptions): Promise<SiteConfig>;
1114
+ }
1115
+ /**
1116
+ * The three services `GET /services_status` actually probes. Mirrors
1117
+ * `ServiceHealthRegistry::EXTERNAL`.
1118
+ */
1119
+ export declare const EXTERNAL_SERVICE_SLUGS: readonly ["vocal_separator", "ai", "yt_dlp"];
1120
+ /**
1121
+ * The eight services that live inside the Rails process and are therefore
1122
+ * "up" whenever the healthcheck job runs at all. They appear in
1123
+ * {@link ServicesStatusNamespace.uptime} and NOT in
1124
+ * {@link ServicesStatusNamespace.current}.
1125
+ */
1126
+ export declare const INTERNAL_SERVICE_SLUGS: readonly ["accounts", "notifications", "storage", "socials", "short_links", "ip_lookup", "jokes", "space_invaders"];
1127
+ /** Every slug the uptime report covers. Mirrors `ServiceHealthRegistry::ALL_SLUGS`. */
1128
+ export declare const ALL_SERVICE_SLUGS: readonly ["accounts", "notifications", "storage", "socials", "short_links", "ip_lookup", "jokes", "space_invaders", "vocal_separator", "ai", "yt_dlp"];
1129
+ /** A service slug. Open, because the registry is a constant somebody will extend. */
1130
+ export type ServiceSlug = (typeof ALL_SERVICE_SLUGS)[number] | (string & {});
1131
+ /** Live health of one external service. */
1132
+ export interface ServiceHealth {
1133
+ /** `true` when the probe got a 2xx from the service's `/health`. */
1134
+ readonly ok: boolean;
1135
+ /**
1136
+ * A STRING, and not the HTTP status you might expect from the name.
1137
+ *
1138
+ * It is `"OK"` when the probe succeeded, and otherwise the failure's
1139
+ * identity: either a Ruby exception class (`"Errno::ECONNREFUSED"`,
1140
+ * `"Net::OpenTimeout"`, `"SocketError"`), or `"HTTP<code>"` for a
1141
+ * non-success response (`"HTTP503"`), or `"MissingURL"` when the service has
1142
+ * no URL configured. Show it, do not parse it - the set is whatever Ruby
1143
+ * happens to raise.
1144
+ *
1145
+ * The web frontend types this field as `number` and adds an `error` key that
1146
+ * the server never sends. Both are wrong; this is the controller's actual
1147
+ * output.
1148
+ */
1149
+ readonly status: string;
1150
+ }
1151
+ /**
1152
+ * The live status map: one entry per external slug, and nothing else.
1153
+ *
1154
+ * Keyed by {@link EXTERNAL_SERVICE_SLUGS} only - the internal services do not
1155
+ * appear, because there is nothing to ping.
1156
+ */
1157
+ export type ServicesStatusMap = Record<string, ServiceHealth>;
1158
+ /** One day of a service's history in the uptime report. */
1159
+ export interface UptimeDay {
1160
+ /** `YYYY-MM-DD`, in the SERVER's timezone - the bucket is `DATE(created_at)`. */
1161
+ readonly date: string;
1162
+ /**
1163
+ * - `"up"` - every ping that day succeeded;
1164
+ * - `"degraded"` - some succeeded and some did not;
1165
+ * - `"down"` - every ping failed;
1166
+ * - `"unknown"` - no pings at all that day (the future half of today, days
1167
+ * before the service existed, and any window where the healthcheck job was
1168
+ * not running).
1169
+ */
1170
+ readonly status: "up" | "degraded" | "down" | "unknown";
1171
+ /** Successful pings that day. Roughly 1440 on a fully healthy day. */
1172
+ readonly up: number;
1173
+ /** Failed pings that day. */
1174
+ readonly down: number;
1175
+ }
1176
+ /** One service's 90-day history. */
1177
+ export interface UptimeService {
1178
+ readonly slug: ServiceSlug;
1179
+ /**
1180
+ * Exactly 90 entries, oldest first, with no gaps: a day with no data is
1181
+ * present with `status: "unknown"` and zero counts rather than missing.
1182
+ * Index 89 is today, and today is partial.
1183
+ */
1184
+ readonly days: UptimeDay[];
1185
+ /**
1186
+ * Successful pings over total pings across the whole window, as a
1187
+ * percentage rounded to two decimals. `null` when the service has no pings
1188
+ * at all in the window - a brand new slug, or a long outage of the
1189
+ * healthcheck job itself. Do not render `null` as `0%`.
1190
+ */
1191
+ readonly uptime_pct: number | null;
1192
+ }
1193
+ /** A note appended to an incident as it progressed. */
1194
+ export interface IncidentUpdate {
1195
+ /**
1196
+ * Free text. Auto-opened incidents post `"investigating"` and `"resolved"`;
1197
+ * a hand-written one can say anything.
1198
+ */
1199
+ readonly status: string;
1200
+ readonly body: string | null;
1201
+ readonly created_at: Timestamp;
1202
+ }
1203
+ /** A public incident on the status page. */
1204
+ export interface Incident {
1205
+ /** An INTEGER. */
1206
+ readonly id: number;
1207
+ /** Auto-opened incidents are titled `"<slug> indisponível"`, in Portuguese. */
1208
+ readonly title: string;
1209
+ readonly body: string | null;
1210
+ /** Mirrors `ServiceIncident::SEVERITIES`. Auto-opened ones are always `"major"`. */
1211
+ readonly severity: "minor" | "major" | "critical";
1212
+ readonly started_at: Timestamp;
1213
+ /** `null` while the incident is open. */
1214
+ readonly resolved_at: Timestamp | null;
1215
+ /** Affected slugs. Can be empty, and can name a slug not in the registry. */
1216
+ readonly services: ServiceSlug[];
1217
+ /** Oldest first. */
1218
+ readonly updates: IncidentUpdate[];
1219
+ }
1220
+ /** The whole uptime report. */
1221
+ export interface UptimeReport {
1222
+ /** First day of the window, `YYYY-MM-DD`. 89 days before `to`. */
1223
+ readonly from: string;
1224
+ /** Today, `YYYY-MM-DD`. */
1225
+ readonly to: string;
1226
+ /** One entry per slug in {@link ALL_SERVICE_SLUGS}, in registry order. */
1227
+ readonly services: UptimeService[];
1228
+ /** The 20 most recent PUBLIC incidents, newest first. Private ones are omitted. */
1229
+ readonly incidents: Incident[];
1230
+ }
1231
+ /** Seconds of server-side caching on {@link ServicesStatusNamespace.uptime}. */
1232
+ export declare const UPTIME_CACHE_SECONDS = 60;
1233
+ /** Days of history the uptime report covers. Mirrors `ServicesStatusController::UPTIME_DAYS`. */
1234
+ export declare const UPTIME_WINDOW_DAYS = 90;
1235
+ /**
1236
+ * The `services_status` namespace: the public status page.
1237
+ *
1238
+ * Both routes are anonymous (`allow_unauthenticated_access` with no `only:`),
1239
+ * and both are exempt from the visitor-logging after-action because the status
1240
+ * widget polls from every page load and would otherwise drown the activity
1241
+ * feed.
1242
+ *
1243
+ * ## The two calls have opposite cost profiles, and the names mislead
1244
+ *
1245
+ * {@link current} sounds cheap and is the expensive one; {@link uptime} sounds
1246
+ * heavy and is served from a cache. Read both method docs before you put
1247
+ * either behind a poller.
1248
+ */
1249
+ export declare class ServicesStatusNamespace extends Resource {
1250
+ /**
1251
+ * `GET /services_status` - live health of the three external services.
1252
+ *
1253
+ * **This is the expensive endpoint in this namespace.** It performs the
1254
+ * probes inline, on the request thread, one after another - the controller
1255
+ * uses `index_with` with `Object#then`, so there is no concurrency - each
1256
+ * with a 2 second connect timeout and a 5 second read timeout. A healthy
1257
+ * call is a few tens of milliseconds; a call while all three are unreachable
1258
+ * holds a Puma thread for up to about 21 seconds and returns
1259
+ * `ok: false` three times.
1260
+ *
1261
+ * There is NO cache and NO dedicated rate limit, so it sits on the general
1262
+ * anonymous budget of 120 requests per minute per IP. Poll it at most once
1263
+ * every 30 seconds or so, and give it a client-side `timeoutMs` well above
1264
+ * the SDK default if your default is short - a slow answer here is the
1265
+ * normal answer during an outage, not a hung request.
1266
+ *
1267
+ * Only {@link EXTERNAL_SERVICE_SLUGS} appear in the map. The internal slugs
1268
+ * are absent because they have nothing to probe; read them out of
1269
+ * {@link uptime} instead.
1270
+ */
1271
+ current(options?: RequestOptions): Promise<ServicesStatusMap>;
1272
+ /**
1273
+ * `GET /services_status/uptime` - 90 days of per-day history for all eleven
1274
+ * services, plus the 20 most recent public incidents.
1275
+ *
1276
+ * ## Cost and freshness, honestly
1277
+ *
1278
+ * This endpoint was flooded (roughly 900 requests a minute from a load
1279
+ * generator) and the fix was not a throttle: it was one grouped query plus a
1280
+ * cache. It still has no rack-attack bucket of its own.
1281
+ *
1282
+ * - **Freshness: up to 60 seconds stale.** The whole payload is memoised
1283
+ * under the single global cache key `"services_status/uptime"` for
1284
+ * {@link UPTIME_CACHE_SECONDS} seconds. The key is not per-caller and not
1285
+ * per-parameter (there are no parameters), so every visitor on the site
1286
+ * shares one entry. Polling faster than once a minute cannot produce a
1287
+ * newer number - it just spends your rate budget re-fetching bytes you
1288
+ * already have.
1289
+ * - **Cost on a hit: sending the payload.** Eleven services times ninety
1290
+ * days is 990 day objects plus the incidents, so this is a
1291
+ * double-digit-kilobyte response every time. There is no `ETag` and no
1292
+ * `Last-Modified`, so it cannot answer `304` even when nothing changed.
1293
+ * - **Cost on a miss: one grouped aggregate** over
1294
+ * `service_pings` - `GROUP BY slug, DATE(created_at), status` across the
1295
+ * window - which is on the order of a million rows, since the healthcheck
1296
+ * job writes one ping per slug per minute. Indexed on
1297
+ * `(slug, created_at)`, but it is still the single heaviest query on the
1298
+ * public surface, and exactly one request per minute pays it.
1299
+ *
1300
+ * ## Reading the numbers
1301
+ *
1302
+ * `up` and `down` are ping counts, not durations: a fully healthy day is
1303
+ * about 1440 up and 0 down. The window is 90 days and ping retention is also
1304
+ * 90 days, so the OLDEST day in every report is partially pruned and its
1305
+ * counts read low - do not compute an SLA off day zero.
1306
+ *
1307
+ * The eight internal slugs are `"up"` for every minute the Rails process was
1308
+ * running the healthcheck job, because that is literally what they measure.
1309
+ * They report the job's liveness, not the feature's.
1310
+ */
1311
+ uptime(options?: RequestOptions): Promise<UptimeReport>;
1312
+ }
1313
+ /**
1314
+ * The twelve service ids the counter accepts. Mirrors
1315
+ * `ServiceUsage::ALLOWED_SERVICE_IDS`, and it is a closed set: anything else
1316
+ * is `400 "Unknown service_id"`.
1317
+ */
1318
+ export declare const SERVICE_USAGE_IDS: readonly ["storage", "tools", "games", "music", "movies", "ai", "account", "tickets", "messages", "blogs", "status", "administration"];
1319
+ /** One of {@link SERVICE_USAGE_IDS}. */
1320
+ export type ServiceUsageId = (typeof SERVICE_USAGE_IDS)[number];
1321
+ /**
1322
+ * A per-user visit counter.
1323
+ *
1324
+ * Note what is NOT here: no `id`, no `user_id`, no timestamps. Both routes
1325
+ * build the JSON by hand (`{ service_id:, count: }`) instead of going through
1326
+ * a blueprint, so this is one of the very few payloads in the API that is not
1327
+ * a record.
1328
+ */
1329
+ export interface ServiceUsage {
1330
+ readonly service_id: ServiceUsageId;
1331
+ /** Lifetime visit count for this user and service. Monotonic, never reset. */
1332
+ readonly count: number;
1333
+ }
1334
+ /**
1335
+ * The `service_usages` namespace: which parts of the site a user opens, so the
1336
+ * home screen can put their favourites first.
1337
+ *
1338
+ * Both routes need a session - there is no `allow_unauthenticated_access` on
1339
+ * this controller - and both are pure bookkeeping. The music app calls
1340
+ * {@link record} with `"music"` on launch, fire and forget.
1341
+ */
1342
+ export declare class ServiceUsagesNamespace extends Resource {
1343
+ /**
1344
+ * `POST /service_usages` - increments the caller's counter for one service.
1345
+ *
1346
+ * Answers **`200`, not `201`**, even on the very first call that creates the
1347
+ * row: the controller uses `ok!` rather than `created!`, so this is one of
1348
+ * the handful of creates in the API that breaks the 201 convention. The body
1349
+ * is the updated `{ service_id, count }`.
1350
+ *
1351
+ * ## Not idempotent, and it can page somebody
1352
+ *
1353
+ * Every call does `count += 1` and stamps `last_visited_at`, so a retry
1354
+ * inflates the number. That is why the transport's default of never
1355
+ * replaying a `POST` is the right default here: do not pass `retry: {}`.
1356
+ *
1357
+ * It can also fire a Discord `service_opened` alert - on the first ever
1358
+ * visit, and again whenever more than an hour has passed since the last one.
1359
+ * A client that calls this on every route change inside an app is fine (the
1360
+ * hour gap suppresses the alert), but a client that calls it from a
1361
+ * background poller is a pager, not telemetry.
1362
+ *
1363
+ * Fire and forget: nothing in a UI should wait on this, and nothing should
1364
+ * fail because it failed.
1365
+ *
1366
+ * @throws {OmsApiError} 400 `"Unknown service_id"` for anything outside
1367
+ * {@link SERVICE_USAGE_IDS}; 401 without a session.
1368
+ */
1369
+ record(serviceId: ServiceUsageId, options?: RequestOptions): Promise<ServiceUsage>;
1370
+ /**
1371
+ * `GET /service_usages/top` - the caller's most-used services, busiest
1372
+ * first, tie-broken by most recently visited.
1373
+ *
1374
+ * Not the list DSL: `limit` is the only parameter, it is clamped to
1375
+ * `1..10` (silently - asking for 50 returns 10), and it defaults to 3. There
1376
+ * is no paging and no way to read the full set.
1377
+ *
1378
+ * Only services the caller has actually opened appear, so a fresh account
1379
+ * gets an empty array rather than every id with a zero.
1380
+ */
1381
+ top(input?: {
1382
+ readonly limit?: number;
1383
+ }, options?: RequestOptions): Promise<ServiceUsage[]>;
1384
+ }
1385
+ /** One day in a daily-count series. Mirrors `DailyBucketStats#daily_series`. */
1386
+ export interface AnalysisDailyPoint {
1387
+ /** `YYYY-MM-DD`, server timezone. */
1388
+ readonly date: string;
1389
+ /** Rows created that day. `0` for a day with none - the series has no gaps. */
1390
+ readonly count: number;
1391
+ }
1392
+ /** Days covered by {@link AnalysisNamespace.filesDaily}. Mirrors `DAILY_WINDOW_DAYS`. */
1393
+ export declare const ANALYSIS_DAILY_WINDOW_DAYS = 30;
1394
+ /**
1395
+ * The `analysis` namespace: two admin reports about storage.
1396
+ *
1397
+ * **Admin only, and the refusal is unusual.** `require_admin!` answers `403`
1398
+ * whose body is a long quotation from Monster House rather than an error code,
1399
+ * so do not try to match on the message - check the status. An anonymous
1400
+ * caller is stopped earlier, by the authentication filter, with the ordinary
1401
+ * `401 "Session required to access this resource."`.
1402
+ *
1403
+ * The route is declared as a full `resources :analysis`, so paths like
1404
+ * `GET /analysis` and `GET /analysis/:id` exist in the router with no action
1405
+ * behind them - `AnalysisController` defines only the two collection actions.
1406
+ * Calling one fails inside Rails with `AbstractController::ActionNotFound`,
1407
+ * which surfaces as a `404` carrying a Rails error page rather than this
1408
+ * API's usual bare string. There are exactly two usable routes here and they
1409
+ * are both below.
1410
+ */
1411
+ export declare class AnalysisNamespace extends Resource {
1412
+ /**
1413
+ * `GET /analysis/storages` - every root directory in the system, with its
1414
+ * recursive size.
1415
+ *
1416
+ * This is `FsNode.directory.root_nodes.render`: every node with no parent,
1417
+ * for every user, which in practice means each account's home, trash and
1418
+ * vault roots. Rendered in the DEFAULT `FsNodeBlueprint` view, which is what
1419
+ * {@link FsNode} describes.
1420
+ *
1421
+ * Two things to expect:
1422
+ *
1423
+ * - **No owner.** The blueprint does not emit `user_id`, so the payload
1424
+ * tells you that a root called `"home"` holds 40 GB and not whose it is.
1425
+ * Correlating means another query.
1426
+ * - **No limit and no paging.** The scope is unbounded, so the response
1427
+ * grows linearly with the number of accounts. It is an admin report, not
1428
+ * something to poll.
1429
+ *
1430
+ * The `size` on a root is the recursive total maintained by the storage
1431
+ * layer; it has drifted from the true sum before, so read it as an estimate.
1432
+ */
1433
+ storages(options?: RequestOptions): Promise<FsNode[]>;
1434
+ /**
1435
+ * `GET /analysis/files_daily` - files created per day over the last
1436
+ * {@link ANALYSIS_DAILY_WINDOW_DAYS} days. Unwraps `{"creations_daily": [...]}`.
1437
+ *
1438
+ * Exactly 30 entries, oldest first, zero-filled: a day with no uploads is
1439
+ * present with `count: 0`. The last entry is today and is partial.
1440
+ *
1441
+ * Counts `fs_nodes` of kind `file` by `DATE(created_at)`, so it measures
1442
+ * node creation and not bytes - a folder copy that mints 50 000 nodes shows
1443
+ * up here as 50 000 files.
1444
+ */
1445
+ filesDaily(options?: RequestOptions): Promise<AnalysisDailyPoint[]>;
1446
+ }
1447
+ /** Primary key of a leaderboard entry. An INTEGER. */
1448
+ export type SpaceInvadersGameId = number;
1449
+ /** Points a single kill can be worth. Mirrors `MAX_POINTS_PER_KILL`. */
1450
+ export declare const SPACE_INVADERS_MAX_POINTS_PER_KILL = 10;
1451
+ /** Kills per second the validator will believe. Mirrors `MAX_KILLS_PER_SECOND`. */
1452
+ export declare const SPACE_INVADERS_MAX_KILLS_PER_SECOND = 1;
1453
+ /** Longest session the validator accepts, in seconds. 24 hours. */
1454
+ export declare const SPACE_INVADERS_MAX_SESSION_SECONDS: number;
1455
+ /** Rows the leaderboard returns. Mirrors the `leaderboard` scope's `limit`. */
1456
+ export declare const SPACE_INVADERS_LEADERBOARD_SIZE = 100;
1457
+ /**
1458
+ * One finished game.
1459
+ *
1460
+ * ## `money` and `time` are STRINGS
1461
+ *
1462
+ * They are `decimal` columns with no precision or scale, and Rails encodes
1463
+ * `BigDecimal` as a JSON string on purpose - a JSON number would be parsed as
1464
+ * a float by most clients and silently lose precision. So the wire carries
1465
+ * `"1200.0"`, not `1200`. `kills` is an `integer` column right next to them
1466
+ * and arrives as a real number.
1467
+ *
1468
+ * The web frontend types all three as `number`, which happens to work only
1469
+ * because it never does arithmetic on them - it interpolates them into a cell.
1470
+ * Anything that sorts, sums or compares these must `Number()` them first;
1471
+ * `"9.0" > "10.0"` is `true` in JavaScript.
1472
+ */
1473
+ export interface SpaceInvadersGame {
1474
+ readonly id: SpaceInvadersGameId;
1475
+ /** The player. A STRING id. */
1476
+ readonly user_id: Id;
1477
+ /** Score. A decimal serialised as a STRING - see the interface docs. */
1478
+ readonly money: string;
1479
+ /** Session length in seconds. Also a decimal serialised as a STRING. */
1480
+ readonly time: string;
1481
+ /** Enemies killed. An integer, and a real JSON number. */
1482
+ readonly kills: number;
1483
+ /**
1484
+ * When the game ended. Stamped SERVER-side from `Time.current` in a
1485
+ * `before_create`, and deliberately not accepted from the request body, so a
1486
+ * client cannot back-date or future-date an entry. Sending it is ignored.
1487
+ */
1488
+ readonly played_at: Timestamp;
1489
+ readonly created_at: Timestamp;
1490
+ readonly updated_at: Timestamp;
1491
+ }
1492
+ /**
1493
+ * Arguments for {@link SpaceInvadersNamespace.submit}.
1494
+ *
1495
+ * All three are required and all three are validated against each other. The
1496
+ * bounds mirror `frontend/public/spaceinvaders/config.json` and are documented
1497
+ * in the model as what they are: not anti-cheat, just a rejection of scores
1498
+ * that are impossible under the game's own rules. The score is
1499
+ * client-authoritative, so anybody willing to call this endpoint by hand can
1500
+ * post any score inside the bounds.
1501
+ */
1502
+ export interface SubmitSpaceInvadersGameInput {
1503
+ /**
1504
+ * Score. Must be `>= 0` and no greater than
1505
+ * `kills * {@link SPACE_INVADERS_MAX_POINTS_PER_KILL}`, or the call is
1506
+ * `400 "Money is impossibly high for N kills"`.
1507
+ */
1508
+ readonly money: number;
1509
+ /**
1510
+ * Session length in seconds. Must be `>= 0` and
1511
+ * `<= {@link SPACE_INVADERS_MAX_SESSION_SECONDS}`.
1512
+ */
1513
+ readonly time: number;
1514
+ /**
1515
+ * Enemies killed. Must be a non-negative integer and no greater than
1516
+ * `ceil(time * {@link SPACE_INVADERS_MAX_KILLS_PER_SECOND})`, or the call is
1517
+ * `400 "Kills are impossibly high for a Ns game"`.
1518
+ */
1519
+ readonly kills: number;
1520
+ }
1521
+ /**
1522
+ * The `space_invaders_games` namespace: the leaderboard for the embedded game.
1523
+ *
1524
+ * The only genuinely public thing here is {@link leaderboard}. Submitting
1525
+ * needs a session, and so - oddly - does {@link list}.
1526
+ */
1527
+ export declare class SpaceInvadersNamespace extends Resource {
1528
+ /**
1529
+ * `GET /space_invaders_games/leaderboard` - the top
1530
+ * {@link SPACE_INVADERS_LEADERBOARD_SIZE} scores, highest `money` first.
1531
+ *
1532
+ * Anonymous callers welcome. Not the list DSL: no paging, no filters, no
1533
+ * ordering - `order(money: :desc).limit(100)` is the whole query, and it is
1534
+ * backed by a descending index on `money`.
1535
+ *
1536
+ * One row per GAME, not per player: a player who posts three good runs
1537
+ * occupies three slots. Deduplicate client-side if you want a per-player
1538
+ * board.
1539
+ *
1540
+ * The rows carry `user_id` and nothing else about the player - no handle, no
1541
+ * avatar - so a board with names needs a separate lookup.
1542
+ */
1543
+ leaderboard(options?: RequestOptions): Promise<SpaceInvadersGame[]>;
1544
+ /**
1545
+ * `GET /space_invaders_games` - every game ever recorded, paged.
1546
+ *
1547
+ * `viewable_by` is `all`, so any signed-in caller enumerates the whole
1548
+ * table, everybody's runs included. It needs a session even though
1549
+ * {@link leaderboard} does not, which is the wrong way round if you were
1550
+ * expecting the listing to be the public one.
1551
+ *
1552
+ * **You cannot filter by player.** The controller declares only
1553
+ * `create_params`, so the search allowlist is the three defaults - `id`,
1554
+ * `created_at`, `updated_at`. `exact_search: { user_id: "..." }` is
1555
+ * `400 "Unknown exact_search filter: user_id"`. To show one player's
1556
+ * history, page and filter client-side, or use `order: "money:desc"` and
1557
+ * stop early.
1558
+ *
1559
+ * No default ordering, so pass one. Sends an `ETag`.
1560
+ */
1561
+ list(params?: ContentListParams, options?: RequestOptions): Promise<Paginated<SpaceInvadersGame>>;
1562
+ /**
1563
+ * `POST /space_invaders_games` - records a finished run. `201`.
1564
+ *
1565
+ * The player is taken from the session and `played_at` is stamped
1566
+ * server-side; neither can be supplied. Any signed-in user may submit.
1567
+ *
1568
+ * Every submission is a new row, so a retry after a lost response posts the
1569
+ * run twice and both appear on the leaderboard. The transport does not
1570
+ * replay a `POST` by default, and this is an endpoint where you should not
1571
+ * ask it to.
1572
+ *
1573
+ * There is no per-user rate limit beyond the general 600/min, and no
1574
+ * de-duplication: two identical runs are two rows.
1575
+ *
1576
+ * @throws {OmsApiError} 400 with the validation sentence when the score
1577
+ * fails the plausibility bounds - see {@link SubmitSpaceInvadersGameInput};
1578
+ * 401 without a session.
1579
+ */
1580
+ submit(input: SubmitSpaceInvadersGameInput, options?: RequestOptions): Promise<SpaceInvadersGame>;
1581
+ /**
1582
+ * `DELETE /space_invaders_games/:id` - removes an entry. `204`.
1583
+ *
1584
+ * The player who set it, or an admin. Anybody else gets
1585
+ * `401 "You are not authorized to destroy this resource"` - and note it is a
1586
+ * 401 rather than a 404, because `viewable_by` is `all` and the lookup
1587
+ * succeeds before the authorisation check.
1588
+ *
1589
+ * There is no update route: the resource is declared
1590
+ * `only: [:create, :index, :destroy]`, so a score can be deleted but never
1591
+ * edited.
1592
+ */
1593
+ destroy(id: SpaceInvadersGameId, options?: RequestOptions): Promise<void>;
1594
+ }
1595
+ /**
1596
+ * The two path prefixes the proxy will forward. Mirrors
1597
+ * `IntelController::ALLOWED_PREFIXES`.
1598
+ *
1599
+ * `api` is the sidecar's JSON surface; `img` is its image surface. Anything
1600
+ * else - and any path containing `..` - is `400 "Invalid intel path."` before
1601
+ * a byte leaves the backend.
1602
+ */
1603
+ export declare const INTEL_ALLOWED_PREFIXES: readonly ["api", "img"];
1604
+ /** Connect timeout the backend applies to the sidecar, in milliseconds. */
1605
+ export declare const INTEL_UPSTREAM_OPEN_TIMEOUT_MS = 5000;
1606
+ /** Read timeout the backend applies to the sidecar, in milliseconds. */
1607
+ export declare const INTEL_UPSTREAM_READ_TIMEOUT_MS = 60000;
1608
+ /**
1609
+ * The `intel` proxy: a read-only passthrough to an internal service.
1610
+ *
1611
+ * ## Why this namespace has no types
1612
+ *
1613
+ * `GET /intel/*path` is not an API. It is a generic forwarder:
1614
+ * `IntelController#proxy` checks who is asking, refuses anything outside
1615
+ * {@link INTEL_ALLOWED_PREFIXES}, joins the rest of the path onto the
1616
+ * sidecar's base URL, replays the query string VERBATIM, injects an
1617
+ * `X-API-Key` that never reaches the browser, and hands back whatever comes
1618
+ * out - the upstream body, the upstream `Content-Type`, the upstream
1619
+ * `Cache-Control` and the upstream STATUS CODE, unexamined.
1620
+ *
1621
+ * The SDK cannot see that service, its routes are not in this repository, and
1622
+ * nothing in the backend validates or reshapes its answers. Publishing typed
1623
+ * `getStories()` / `getReport()` methods here would be inventing a contract
1624
+ * nobody can hold up, and it would rot the first time the sidecar changed. So
1625
+ * this namespace offers exactly what the endpoint offers: a path, a query bag,
1626
+ * and a caller-supplied result type it is the CALLER's job to justify.
1627
+ *
1628
+ * If you want typed intel data, use the native Rails endpoints instead -
1629
+ * they are real controllers with real blueprints, the web frontend moved onto
1630
+ * them and left this proxy behind, and they ARE wrapped, one family per
1631
+ * sub-namespace under {@link IntelNamespace}. This class is only for the
1632
+ * embedded hub the old page still renders and for its image bytes.
1633
+ *
1634
+ * ## Access
1635
+ *
1636
+ * Effectively a single-user endpoint. `Intel::Access.allowed?` is an admin
1637
+ * check OR a hard-coded handle allowlist, so every other authenticated caller
1638
+ * gets `403 "Intel access is restricted."` and an anonymous one gets the
1639
+ * ordinary `401` first. Do not build a shared feature on this.
1640
+ *
1641
+ * ## Errors are not the API's errors
1642
+ *
1643
+ * The upstream status is forwarded as-is, so a 4xx or 5xx here carries the
1644
+ * SIDECAR's body - which may be JSON, may be HTML, may be empty, and is
1645
+ * certainly not this API's usual bare JSON string. Read
1646
+ * {@link OmsApiError.body} defensively.
1647
+ *
1648
+ * When the sidecar cannot be reached at all - refused connection, DNS
1649
+ * failure, or a timeout past
1650
+ * {@link INTEL_UPSTREAM_OPEN_TIMEOUT_MS} / {@link INTEL_UPSTREAM_READ_TIMEOUT_MS} -
1651
+ * the backend answers `502 "Intel service unreachable."`, which is a normal
1652
+ * bare-string error.
1653
+ *
1654
+ * Only `GET` is routed. There is no way to write anything through this proxy.
1655
+ *
1656
+ * @deprecated Intel now lives ENTIRELY inside Rails. The analysis pipeline
1657
+ * moved into Ruby and Solid Queue, and sources became per-user records with
1658
+ * sandboxed TS scripts, so the `omelhorsite-intel-analise` sidecar this
1659
+ * forwards to is on its way out. The route still answers today, which is why
1660
+ * this class is still here rather than deleted, but nothing new should be
1661
+ * built on it: when the sidecar goes, every call through here becomes a
1662
+ * `502 "Intel service unreachable."` with no deprecation window, because the
1663
+ * backend cannot tell a retired sidecar from a broken one.
1664
+ *
1665
+ * Use the typed families under {@link IntelNamespace} instead: `articles`,
1666
+ * `reports`, `sources`, `scripts`, `items`, `config` and `stats` are real
1667
+ * controllers with real blueprints over the API's own tables.
1668
+ */
1669
+ export declare class IntelProxyNamespace extends Resource {
1670
+ /**
1671
+ * `GET /intel/<path>` - forwards a read and parses the answer as JSON.
1672
+ *
1673
+ * The type parameter is a PROMISE YOU are making, not one the SDK or the
1674
+ * backend can check. Default it to `unknown` and narrow at the call site
1675
+ * unless you own the sidecar's route.
1676
+ *
1677
+ * `path` is relative and must start with `api/` or `img/`
1678
+ * (see {@link INTEL_ALLOWED_PREFIXES}). A leading slash is stripped, and
1679
+ * each segment is percent-encoded while the separators are kept, so
1680
+ * `"api/articles/abc def"` reaches the sidecar as `api/articles/abc%20def`.
1681
+ * Pass an unencoded path; passing a pre-encoded one double-encodes it.
1682
+ *
1683
+ * `query` is encoded by the SDK's normal rules and then replayed to the
1684
+ * sidecar untouched, `null` sentinel and all - which is worth knowing,
1685
+ * because the sentinel is a Rails convention the sidecar has never heard of.
1686
+ * Prefer plain values here.
1687
+ *
1688
+ * The route is declared `format: false`, so a trailing `.json` stays part of
1689
+ * the path instead of being read as a Rails format.
1690
+ *
1691
+ * If the answer is not JSON it comes back as the raw text (the transport
1692
+ * falls back to a string rather than throwing), so a `T` of `unknown`
1693
+ * genuinely can be a `string`.
1694
+ *
1695
+ * Cost: the backend buffers the entire upstream body in memory before
1696
+ * sending it on - there is no streaming - and holds a Puma thread for up to
1697
+ * {@link INTEL_UPSTREAM_READ_TIMEOUT_MS} while it waits.
1698
+ *
1699
+ * @throws {OmsApiError} 400 `"Invalid intel path."` for a path outside the
1700
+ * allowed prefixes or containing `..`; 403 `"Intel access is restricted."`;
1701
+ * 502 `"Intel service unreachable."`; or anything at all, forwarded from
1702
+ * the sidecar.
1703
+ */
1704
+ get<T = unknown>(path: string, query?: QueryParams, options?: RequestOptions): Promise<T>;
1705
+ /**
1706
+ * `GET /intel/<path>` - forwards a read and keeps the bytes.
1707
+ *
1708
+ * For the `img/` prefix, and for any `api/` route that answers with
1709
+ * something other than JSON. Buffers the whole body, so do not point it at
1710
+ * anything large.
1711
+ *
1712
+ * {@link FileOutput.filename} will be `undefined`: the backend sends
1713
+ * `Content-Disposition: inline` with no filename. `contentType` is whatever
1714
+ * the sidecar declared.
1715
+ */
1716
+ fetch(path: string, query?: QueryParams, options?: RequestOptions): Promise<FileOutput>;
1717
+ /**
1718
+ * The absolute URL of a proxied path, for an `<img src>` or an `<a href>`.
1719
+ *
1720
+ * Builds the string and makes no request. Useful only for a cookie-session
1721
+ * browser client on the API's origin: the proxy requires an authenticated,
1722
+ * allowlisted caller, so a bare URL opened without a credential is a `401`.
1723
+ */
1724
+ url(path: string, query?: QueryParams): string;
1725
+ }
1726
+ /**
1727
+ * ## Why these ARE typed, when the proxy above is not
1728
+ *
1729
+ * The two halves of "intel" look alike and are nothing alike.
1730
+ * {@link IntelProxyNamespace} forwards `GET /intel/*path` to a service whose
1731
+ * routes are not in the backend repository, so its answers are genuinely
1732
+ * unknowable. Everything below is the opposite: seven ordinary Rails
1733
+ * controllers over eight of the backend's own tables, each with a Blueprinter
1734
+ * blueprint that names every key it emits. `IntelArticleBlueprint`,
1735
+ * `IntelReportBlueprint`, `IntelSourceBlueprint`, `IntelScriptBlueprint`,
1736
+ * `IntelItemBlueprint` and `IntelConfigBlueprint` are the contract, and
1737
+ * `IntelStatsController#show` hand-writes its hash literally. Nothing here is
1738
+ * forwarded, nothing here is opaque, and the SDK types it the way it types any
1739
+ * other resource.
1740
+ *
1741
+ * Where the two meet: the sidecar behind the proxy is the OLD, separate intel
1742
+ * product embedded in the web app's `/intel` page. The routes below are the one
1743
+ * that replaced it. Reach for these first; the proxy is for the embedded hub
1744
+ * and for image bytes.
1745
+ *
1746
+ * ## Access: this is effectively a one-user feature
1747
+ *
1748
+ * `IntelAccess` runs `before_action :require_intel_access` on all seven
1749
+ * controllers, and `Intel::Access.allowed?` is `user.admin? ||
1750
+ * ALLOWED_HANDLES.include?(user.handle)` with `ALLOWED_HANDLES` frozen to a
1751
+ * single handle in the source. So: anonymous is `401`, any other signed-in
1752
+ * account is `403 "Intel access is restricted."`, and no amount of correct
1753
+ * request shaping changes that. Do not build a shared feature on it, and do not
1754
+ * treat a 403 here as a bug in the caller.
1755
+ *
1756
+ * As with everything else in this file, no controller declares an
1757
+ * `oauth_scope`, so an OAuth access token is `403 {"error":"insufficient_scope"}`
1758
+ * on every route below. Session credential only.
1759
+ *
1760
+ * ## Ids are STRINGS here, unlike the rest of this file
1761
+ *
1762
+ * Both intel migrations create every table with `id: :string`, so articles,
1763
+ * reports, sources, scripts, items and the config row all carry opaque string
1764
+ * ids - while blogs, notifications, jokes and Space Invaders games two hundred
1765
+ * lines up are integers. Nothing in intel is ever a number you can compare or
1766
+ * sort by.
1767
+ *
1768
+ * ## Ceilings
1769
+ *
1770
+ * None of their own. Every route rides the general bucket: 600 requests per
1771
+ * minute for an authenticated caller. Two of them are still expensive and are
1772
+ * documented as such - {@link IntelStatsNamespace.get} and
1773
+ * {@link IntelSourcesNamespace.run}.
1774
+ */
1775
+ /** Categories `IntelArticle::CATEGORIES` allows. `null` when the classifier declined to pick one. */
1776
+ export declare const INTEL_ARTICLE_CATEGORIES: readonly ["incidente", "politica", "comunidade", "sociedade", "internacional", "economia", "outro"];
1777
+ /**
1778
+ * A story's category.
1779
+ *
1780
+ * Widened with `string & {}` deliberately: the list is a Ruby constant that a
1781
+ * migration can extend without the SDK noticing, and a `switch` that fails to
1782
+ * compile on a new category is worse than one that falls through to a default.
1783
+ * The backend DOES validate inclusion, so a value outside the list can only
1784
+ * mean the constant moved.
1785
+ */
1786
+ export type IntelArticleCategory = (typeof INTEL_ARTICLE_CATEGORIES)[number] | (string & {});
1787
+ /** Report windows `IntelReport::KINDS` allows. */
1788
+ export declare const INTEL_REPORT_KINDS: readonly ["6h", "day", "week", "month"];
1789
+ /** Which window a report covers. */
1790
+ export type IntelReportKind = (typeof INTEL_REPORT_KINDS)[number] | (string & {});
1791
+ /** The three values `IntelSource::HEALTHS` allows. */
1792
+ export declare const INTEL_SOURCE_HEALTHS: readonly ["unknown", "ok", "error"];
1793
+ /** Health of a source's last run. `"unknown"` until it has ever run. */
1794
+ export type IntelSourceHealth = (typeof INTEL_SOURCE_HEALTHS)[number];
1795
+ /**
1796
+ * The only keys {@link IntelConfig.prompts} accepts, from
1797
+ * `IntelConfig::PROMPT_KEYS`.
1798
+ *
1799
+ * Any other key fails the whole `PATCH` with
1800
+ * `400 "Prompts unknown keys: <the offenders>"`. A key that is present but
1801
+ * empty is not the same as an absent one: absent means "use the platform
1802
+ * default", present-and-empty means the pipeline gets an empty prompt.
1803
+ */
1804
+ export declare const INTEL_PROMPT_KEYS: readonly ["build", "enrich_plan", "enrich_actors", "enrich_synth", "report"];
1805
+ /** One overridable prompt in the analysis pipeline. */
1806
+ export type IntelPromptKey = (typeof INTEL_PROMPT_KEYS)[number];
1807
+ /**
1808
+ * Consecutive failures after which `IntelSource#register_failure!` flips
1809
+ * `enabled` to `false` by itself. Mirrors `IntelSource::DISABLE_AFTER_FAILURES`.
1810
+ *
1811
+ * Nothing turns it back on: a source that hit this stays off until someone
1812
+ * `update()`s `enabled` back to `true`. That is what
1813
+ * {@link IntelSource.consecutive_failures} is for - watch it, do not wait for
1814
+ * an alert.
1815
+ */
1816
+ export declare const INTEL_SOURCE_DISABLE_AFTER_FAILURES = 20;
1817
+ /** Largest script body `IntelScript` will store, from `IntelScript::MAX_CODE_BYTES`. */
1818
+ export declare const INTEL_SCRIPT_MAX_CODE_BYTES: number;
1819
+ /**
1820
+ * A story: several raw items about the same event, grouped, scored and
1821
+ * categorised by the analysis pipeline.
1822
+ *
1823
+ * This is the shape an INDEX row has. `GET /intel_articles/:id` renders
1824
+ * `:extended`, which is this plus four more keys - see
1825
+ * {@link IntelArticleDetail}. Blueprinter views inherit, so the detail is
1826
+ * always a superset, never a different record.
1827
+ */
1828
+ export interface IntelArticle {
1829
+ readonly id: Id;
1830
+ readonly created_at: Timestamp;
1831
+ readonly updated_at: Timestamp;
1832
+ /** Headline the model wrote. Nullable: the column has no `NOT NULL`. */
1833
+ readonly title: string | null;
1834
+ /** One-paragraph summary. Nullable for the same reason. */
1835
+ readonly summary: string | null;
1836
+ /**
1837
+ * 0-10, validated `only_integer, in: 0..10`. The buckets the dashboard uses
1838
+ * are in {@link IntelStats.by_importance} and they are NOT evenly spaced:
1839
+ * >=9 critical, 7-8 high, 5-6 medium, 3-4 low, <3 noise.
1840
+ */
1841
+ readonly importance: number;
1842
+ /** See {@link IntelArticleCategory}. `null` when unclassified. */
1843
+ readonly category: IntelArticleCategory | null;
1844
+ /**
1845
+ * Free-form tags. The column defaults to `[]`, but it is nullable, so a row
1846
+ * written before the default landed can still hand you `null`. Do not map
1847
+ * over it without a guard.
1848
+ */
1849
+ readonly tags: string[] | null;
1850
+ /**
1851
+ * The `og:image` of one of the story's sources, stored RAW and uncompressed
1852
+ * - it points at whatever news site published it, not at this API. Render it
1853
+ * through {@link intelArticleImageUrl} rather than directly; that helper
1854
+ * explains the trade it makes.
1855
+ */
1856
+ readonly image_url: string | null;
1857
+ /**
1858
+ * Whether the web-search enrichment pass has run on this story.
1859
+ *
1860
+ * `false` is not a failure, it is a queue position: `AnalyzeUserJob` enriches
1861
+ * at most three stories per run, only those at or above
1862
+ * {@link IntelConfig.enrich_min_importance}, and only while
1863
+ * {@link IntelConfig.web_search} is on. A low-importance story stays `false`
1864
+ * for ever, by design.
1865
+ */
1866
+ readonly enriched: boolean;
1867
+ /** When the story was first built. */
1868
+ readonly first_seen_at: Timestamp;
1869
+ /** Touched every time a new item joins the story. This is the "recency" clock. */
1870
+ readonly last_seen_at: Timestamp;
1871
+ /**
1872
+ * How many raw items back this story.
1873
+ *
1874
+ * Computed in the blueprint as `article.intel_article_sources.size`, which
1875
+ * means one COUNT query per row unless the association is already loaded -
1876
+ * and the index does not preload it. A page of 500 stories is 500 extra
1877
+ * queries. This is the reason to keep `pageSize` modest on
1878
+ * {@link IntelArticlesNamespace.list}.
1879
+ */
1880
+ readonly n_sources: number;
1881
+ }
1882
+ /** One raw item cited by a story, as `:extended` inlines it. */
1883
+ export interface IntelArticleSourceRef {
1884
+ /** Id of the {@link IntelItem}. Fetch the full row with `items.get(id)`. */
1885
+ readonly id: Id;
1886
+ /** Name of the {@link IntelSource} the item came from, or `null` if it was deleted. */
1887
+ readonly source_name: string | null;
1888
+ readonly title: string | null;
1889
+ readonly url: string | null;
1890
+ readonly published_at: Timestamp | null;
1891
+ }
1892
+ /**
1893
+ * A story related to this one, as `:extended` inlines it.
1894
+ *
1895
+ * "Related" is not "duplicate": duplicates are merged during dedup and never
1896
+ * become two rows. `IntelArticleLink` is an undirected edge between two
1897
+ * DISTINCT stories, which is why {@link relation} is one label describing the
1898
+ * pair rather than a direction.
1899
+ */
1900
+ export interface IntelRelatedArticleRef {
1901
+ readonly id: Id;
1902
+ readonly title: string | null;
1903
+ readonly importance: number;
1904
+ readonly category: IntelArticleCategory | null;
1905
+ /** Free text the model wrote for the edge, e.g. a pattern name. Nullable. */
1906
+ readonly relation: string | null;
1907
+ }
1908
+ /** A report this story appears in, as `:extended` inlines it. Newest period first. */
1909
+ export interface IntelArticleReportRef {
1910
+ readonly id: Id;
1911
+ readonly kind: IntelReportKind;
1912
+ readonly title: string | null;
1913
+ readonly period_end: Timestamp;
1914
+ }
1915
+ /**
1916
+ * `GET /intel_articles/:id` - the `:extended` view.
1917
+ *
1918
+ * Four keys the listing does not carry, and all four are joins the blueprint
1919
+ * runs inline: `sources` walks `intel_items`, `related` walks the link table in
1920
+ * BOTH directions, `reports` orders the report join by `period_end`. There is
1921
+ * no paging on any of them, so a story that has been running for a week can
1922
+ * inline a lot of rows.
1923
+ */
1924
+ export interface IntelArticleDetail extends IntelArticle {
1925
+ /** The long body. `null` until the enrichment pass writes one. */
1926
+ readonly details: string | null;
1927
+ /** Every raw item behind the story. Length matches {@link IntelArticle.n_sources}. */
1928
+ readonly sources: IntelArticleSourceRef[];
1929
+ /** Stories linked to this one. `[]` when the linker found nothing. */
1930
+ readonly related: IntelRelatedArticleRef[];
1931
+ /** Reports that cited this story, newest period first. */
1932
+ readonly reports: IntelArticleReportRef[];
1933
+ }
1934
+ /** A story as a report inlines it. Four keys, no summary and no body. */
1935
+ export interface IntelReportArticleRef {
1936
+ readonly id: Id;
1937
+ readonly title: string | null;
1938
+ readonly importance: number;
1939
+ readonly category: IntelArticleCategory | null;
1940
+ }
1941
+ /**
1942
+ * A generated report over one closed time window.
1943
+ *
1944
+ * The index shape. `GET /intel_reports/:id` adds three keys - see
1945
+ * {@link IntelReportDetail}.
1946
+ *
1947
+ * There is at most ONE report per `(user, kind, period_end)`: the migration
1948
+ * puts a unique index on that triple precisely so a re-run of
1949
+ * `GenerateReportJob` cannot mint a duplicate. Windows are the last CLOSED
1950
+ * period, computed by `IntelReport.last_window`, so a `"day"` report covers
1951
+ * yesterday and never the day in progress.
1952
+ */
1953
+ export interface IntelReport {
1954
+ readonly id: Id;
1955
+ readonly created_at: Timestamp;
1956
+ readonly updated_at: Timestamp;
1957
+ /** Which window: see {@link INTEL_REPORT_KINDS}. */
1958
+ readonly kind: IntelReportKind;
1959
+ /** Title the model wrote. Nullable. */
1960
+ readonly title: string | null;
1961
+ /** Start of the window, inclusive. */
1962
+ readonly period_start: Timestamp;
1963
+ /** End of the window, exclusive. Also the sort key of the listing. */
1964
+ readonly period_end: Timestamp;
1965
+ /** LLM that wrote it, as configured at generation time. Nullable. */
1966
+ readonly model: string | null;
1967
+ }
1968
+ /** `GET /intel_reports/:id` - the `:extended` view. */
1969
+ export interface IntelReportDetail extends IntelReport {
1970
+ /** The report body, usually Markdown. `null` if generation failed halfway. */
1971
+ readonly content: string | null;
1972
+ /**
1973
+ * Whatever the generator chose to record about the run. A free-form JSON
1974
+ * object with no schema on either side, defaulting to `{}` - which is why it
1975
+ * is typed as a bag rather than as fields. Read it defensively.
1976
+ */
1977
+ readonly stats: Record<string, Json> | null;
1978
+ /** The stories the report covered, most important first. */
1979
+ readonly articles: IntelReportArticleRef[];
1980
+ }
1981
+ /**
1982
+ * A configured feed: a script plus the settings that script needs.
1983
+ *
1984
+ * A source is polled by `PollDispatcherJob` once every
1985
+ * {@link poll_interval_minutes}, and each poll writes {@link IntelItem} rows
1986
+ * that the analysis pipeline later turns into stories.
1987
+ */
1988
+ export interface IntelSource {
1989
+ readonly id: Id;
1990
+ readonly created_at: Timestamp;
1991
+ readonly updated_at: Timestamp;
1992
+ /** Up to 200 characters, whitespace-trimmed by the model, unique per user. */
1993
+ readonly name: string;
1994
+ /**
1995
+ * The script's own settings - a URL, a channel, a CSS selector. There is no
1996
+ * schema: the backend permits `config: {}`, meaning an arbitrary object, and
1997
+ * the SCRIPT decides what it reads out of it. What belongs in here is
1998
+ * documented by the script, not by this API.
1999
+ */
2000
+ readonly config: Record<string, Json>;
2001
+ /** Which {@link IntelScript} fetches this source. */
2002
+ readonly intel_script_id: Id;
2003
+ /** Minutes between polls. Validated `in: 5..1440`. */
2004
+ readonly poll_interval_minutes: number;
2005
+ /**
2006
+ * Whether the dispatcher will poll it.
2007
+ *
2008
+ * Can flip to `false` WITHOUT anyone asking: see
2009
+ * {@link INTEL_SOURCE_DISABLE_AFTER_FAILURES}.
2010
+ */
2011
+ readonly enabled: boolean;
2012
+ /**
2013
+ * Incremental cursor the script returned last time - a timestamp, an etag, a
2014
+ * last-seen id, whatever that script uses. Opaque to everything but the
2015
+ * script. Writable, so clearing it is how you force a full re-fetch.
2016
+ */
2017
+ readonly cursor: string | null;
2018
+ /** Result of the last run. `"unknown"` until it has run once. */
2019
+ readonly health: IntelSourceHealth;
2020
+ /** Failure message from the last failed run, truncated to 1000 characters. */
2021
+ readonly last_error: string | null;
2022
+ /** When the source last ran, successfully or not. */
2023
+ readonly last_run_at: Timestamp | null;
2024
+ /** When it last SUCCEEDED. A gap between the two is the thing to alert on. */
2025
+ readonly last_success_at: Timestamp | null;
2026
+ /** Reset to 0 on any success. See {@link INTEL_SOURCE_DISABLE_AFTER_FAILURES}. */
2027
+ readonly consecutive_failures: number;
2028
+ }
2029
+ /**
2030
+ * A TypeScript fetcher that knows how to pull items out of one kind of feed.
2031
+ *
2032
+ * Runs in the `intel-runner` sidecar, inside a V8 isolate with nothing but the
2033
+ * injected `ctx`. Two populations share this table:
2034
+ *
2035
+ * - **built-ins** (`builtin: true`, `user_id: null`, `slug` set) are managed by
2036
+ * `Intel::BuiltinScripts`, visible to everyone, and immutable over HTTP;
2037
+ * - **user scripts** (`builtin: false`, `user_id` set, `slug: null`) are yours.
2038
+ *
2039
+ * `viewable_by` is `builtin OR mine`, so a listing mixes the two. Check
2040
+ * {@link builtin} before offering an edit affordance - see
2041
+ * {@link IntelScriptsNamespace.update} for what happens if you do not.
2042
+ */
2043
+ export interface IntelScript {
2044
+ readonly id: Id;
2045
+ readonly created_at: Timestamp;
2046
+ readonly updated_at: Timestamp;
2047
+ /** Up to 120 characters, whitespace-trimmed by the model. */
2048
+ readonly name: string;
2049
+ /** Stable handle, e.g. `"rss"`. Non-null for built-ins ONLY; always `null` for yours. */
2050
+ readonly slug: string | null;
2051
+ readonly description: string | null;
2052
+ /** `true` for a platform script. Immutable, and not yours to delete. */
2053
+ readonly builtin: boolean;
2054
+ /** Owner. `null` exactly when {@link builtin} is `true`. */
2055
+ readonly user_id: Id | null;
2056
+ /**
2057
+ * The source code - **only on the `:extended` view**.
2058
+ *
2059
+ * `IntelScriptBlueprint` puts `code` inside `view :extended`, so `get()`,
2060
+ * `create()` and `update()` carry it and `list()` does not. That is a
2061
+ * deliberate weight decision (a listing of 64 KiB bodies), not an oversight,
2062
+ * and it is why this key is optional. A row from `list()` has it `undefined`;
2063
+ * fetch the script by id when you actually need the body.
2064
+ */
2065
+ readonly code?: string;
2066
+ }
2067
+ /**
2068
+ * A raw item, exactly as a script returned it.
2069
+ *
2070
+ * Written only by `Intel::FetchSourceJob`; over HTTP it is read-only plus a
2071
+ * delete. Items are the substrate the stories are built from - the story never
2072
+ * copies the body, it points here.
2073
+ */
2074
+ export interface IntelItem {
2075
+ readonly id: Id;
2076
+ readonly created_at: Timestamp;
2077
+ readonly updated_at: Timestamp;
2078
+ /** Which source produced it. */
2079
+ readonly intel_source_id: Id;
2080
+ /**
2081
+ * The script's own id for this item, unique per source. This is the
2082
+ * de-duplication key: a second poll that returns the same `external_id` does
2083
+ * not create a second row.
2084
+ */
2085
+ readonly external_id: string;
2086
+ readonly title: string | null;
2087
+ /** The body the script extracted. Can be large; a listing carries all of it. */
2088
+ readonly content: string | null;
2089
+ readonly url: string | null;
2090
+ readonly author: string | null;
2091
+ /** Publication time as the feed reported it, not as we saw it. */
2092
+ readonly published_at: Timestamp | null;
2093
+ /** When the poll that produced this item ran. Never null. */
2094
+ readonly fetched_at: Timestamp;
2095
+ }
2096
+ /**
2097
+ * The per-user knobs on the analysis pipeline. One row per user, created on
2098
+ * demand - see {@link IntelConfigNamespace.get}.
2099
+ */
2100
+ export interface IntelConfig {
2101
+ readonly id: Id;
2102
+ readonly created_at: Timestamp;
2103
+ readonly updated_at: Timestamp;
2104
+ /**
2105
+ * Free text telling the classifier what "important" means for you. `null`
2106
+ * falls back to the platform default. This is the single highest-leverage
2107
+ * field here: everything else is a threshold applied to the score this
2108
+ * produces.
2109
+ */
2110
+ readonly rubric: string | null;
2111
+ /**
2112
+ * Prompt overrides, keyed by {@link IntelPromptKey}. `{}` means "platform
2113
+ * defaults everywhere"; a key present means that one stage is overridden.
2114
+ *
2115
+ * Nullable at the database level even though it defaults to `{}`.
2116
+ */
2117
+ readonly prompts: Partial<Record<IntelPromptKey, string>> | null;
2118
+ /** LLM for the story-building pass. `null` uses the platform default. */
2119
+ readonly build_model: string | null;
2120
+ /** LLM for report generation. `null` uses the platform default. */
2121
+ readonly report_model: string | null;
2122
+ /** Stories below this importance are left out of reports. 0-10, default 4. */
2123
+ readonly report_min_importance: number;
2124
+ /**
2125
+ * Stories below this importance are never web-enriched. 0-10, default 6.
2126
+ *
2127
+ * Lowering it does not enrich the backlog quickly: the job does three
2128
+ * stories per run, highest importance first.
2129
+ */
2130
+ readonly enrich_min_importance: number;
2131
+ /**
2132
+ * Master switch for the enrichment pass. `false` leaves every story at
2133
+ * `enriched: false` and `details: null` for ever.
2134
+ */
2135
+ readonly web_search: boolean;
2136
+ /**
2137
+ * How many {@link IntelSource} rows you may own. 1-500, default 50.
2138
+ *
2139
+ * Enforced on CREATE only (`validate :within_source_quota, on: :create`), so
2140
+ * lowering it below your current count does not delete anything - it just
2141
+ * stops the next create with `400 "Source limit reached (N)"`.
2142
+ */
2143
+ readonly max_sources: number;
2144
+ }
2145
+ /** One category bucket of {@link IntelStats}. */
2146
+ export interface IntelCategoryCount {
2147
+ readonly category: string;
2148
+ /** Named `c`, not `count` - the controller builds this hash by hand. */
2149
+ readonly c: number;
2150
+ }
2151
+ /** One day of the {@link IntelStats} histogram. */
2152
+ export interface IntelDayCount {
2153
+ /** `YYYY-MM-DD`, from Postgres `DATE(last_seen_at)`. Not a full timestamp. */
2154
+ readonly day: string;
2155
+ readonly c: number;
2156
+ }
2157
+ /**
2158
+ * The importance histogram, with the backend's own Portuguese bucket names.
2159
+ *
2160
+ * The boundaries are hard-coded in `IntelStatsController` and are not
2161
+ * configurable: `critico` >=9, `alta` 7-8, `media` 5-6, `baixa` 3-4, `ruido`
2162
+ * <3. Note they are NOT the same thresholds as
2163
+ * {@link IntelConfig.report_min_importance} or
2164
+ * {@link IntelConfig.enrich_min_importance} - those are yours, these are the
2165
+ * dashboard's.
2166
+ */
2167
+ export interface IntelImportanceBuckets {
2168
+ readonly critico: number;
2169
+ readonly alta: number;
2170
+ readonly media: number;
2171
+ readonly baixa: number;
2172
+ readonly ruido: number;
2173
+ }
2174
+ /** Row counts on the {@link IntelStats} answer. */
2175
+ export interface IntelStatsTotals {
2176
+ /** Stories you own. */
2177
+ readonly articles: number;
2178
+ /**
2179
+ * **Not the number of feeds you have configured.** The controller counts
2180
+ * `IntelArticleSource`, the story-to-item POINTER table, so this is "how
2181
+ * many citations exist across all my stories" and it grows without bound as
2182
+ * stories accumulate. If you want the number of configured sources, read the
2183
+ * length of {@link IntelSourcesNamespace.list}. The name is the backend's and
2184
+ * the SDK does not rename it, but do not put it under a "Sources" label.
2185
+ */
2186
+ readonly sources: number;
2187
+ readonly reports: number;
2188
+ /** Raw items you own, processed or not. */
2189
+ readonly items: number;
2190
+ /**
2191
+ * Items the analysis pipeline has not consumed yet (`processed_at IS NULL`).
2192
+ *
2193
+ * The one number worth watching: a figure that climbs and never falls means
2194
+ * the pipeline is not running - most often because `Intel::LlmClient` is
2195
+ * disabled for want of an API key, in which case `AnalysisDispatcherJob`
2196
+ * returns immediately and silently.
2197
+ */
2198
+ readonly pending_items: number;
2199
+ }
2200
+ /**
2201
+ * `GET /intel_stats` - counters for the intel dashboard.
2202
+ *
2203
+ * Hand-built in the controller rather than rendered by a blueprint, so it has
2204
+ * no `id`, no timestamps and no `:extended` view.
2205
+ */
2206
+ export interface IntelStats {
2207
+ readonly totals: IntelStatsTotals;
2208
+ /**
2209
+ * Categories by story count, descending. Stories with a `null` category are
2210
+ * EXCLUDED, so these do not sum to `totals.articles`.
2211
+ */
2212
+ readonly by_category: IntelCategoryCount[];
2213
+ /**
2214
+ * The last 30 days by `last_seen_at`, ascending.
2215
+ *
2216
+ * Sparse: a day with no activity is simply ABSENT, not present with zero.
2217
+ * Fill the gaps before plotting or the line will lie about its own x-axis.
2218
+ */
2219
+ readonly by_day: IntelDayCount[];
2220
+ readonly by_importance: IntelImportanceBuckets;
2221
+ /** Stories touched in the last 24 hours. */
2222
+ readonly last24h: number;
2223
+ }
2224
+ /** Filters for {@link IntelArticlesNamespace.list}. */
2225
+ export interface ListIntelArticlesParams extends ContentListParams {
2226
+ /**
2227
+ * Free-text search over `title`, `summary` AND `details`.
2228
+ *
2229
+ * A TOP-LEVEL parameter, not a `search` key: the controller reads
2230
+ * `params[:q]` itself, which is why it can reach `details` (a column that is
2231
+ * not in `search_params` at all) and why an unknown-filter 400 cannot
2232
+ * happen for it.
2233
+ *
2234
+ * Three ways it differs from {@link ContentListParams.search}:
2235
+ *
2236
+ * - it is **accent-SENSITIVE**. The controller does `LOWER(col) LIKE
2237
+ * LOWER(term)`, with no unaccenting, while the list DSL's `search` strips
2238
+ * accents on both sides. `"policia"` will not find `"polícia"` here.
2239
+ * - `%` and `_` in your term are **not escaped**. The controller wraps the
2240
+ * term as `"%#{q}%"` and binds it, so a term containing `%` is a wildcard,
2241
+ * not a literal percent sign. Not an injection - it is a bound parameter -
2242
+ * but a surprise. Strip them if you are passing user input through.
2243
+ * - it is an unanchored `LIKE` over three text columns with no index, so it
2244
+ * is a sequential scan of your stories. Fine for thousands, not for
2245
+ * millions.
2246
+ */
2247
+ readonly q?: string;
2248
+ /**
2249
+ * Keep only stories at or above this importance. Also top-level.
2250
+ *
2251
+ * Sent through Ruby's `String#to_i`, which does NOT raise: `"high"` becomes
2252
+ * `0` and the filter silently matches everything. Pass a number and let the
2253
+ * SDK stringify it.
2254
+ */
2255
+ readonly minImportance?: number;
2256
+ /**
2257
+ * `"recent"` orders by `last_seen_at` descending. Anything else - including
2258
+ * omitting it - orders by `importance` descending, then `last_seen_at`
2259
+ * descending. There is no third value and no ascending variant.
2260
+ *
2261
+ * If you ALSO pass {@link PageParams.order}, both apply and yours wins: the
2262
+ * controller appends its ordering after the list DSL has applied
2263
+ * `modifiers[order]`, so your column becomes the primary sort key and the
2264
+ * controller's becomes the tie-breaker. That is the opposite of what the
2265
+ * parameter names suggest.
2266
+ */
2267
+ readonly sort?: "recent" | "importance";
2268
+ }
2269
+ /** Filters for {@link IntelReportsNamespace.list}. */
2270
+ export interface ListIntelReportsParams extends ContentListParams {
2271
+ /**
2272
+ * Narrow to one window, e.g. `"day"`. Sent as `exact_search[kind]`, so it is
2273
+ * equality rather than a prefix match - `"6h"` will not also match `"6hx"`.
2274
+ *
2275
+ * Passing it through {@link ContentListParams.search} instead would be a
2276
+ * partial match and would work too; `kind` is on this controller's
2277
+ * `search_params` allowlist. Equality is what you want.
2278
+ */
2279
+ readonly kind?: IntelReportKind;
2280
+ }
2281
+ /** Filters for {@link IntelSourcesNamespace.list}. */
2282
+ export interface ListIntelSourcesParams extends ContentListParams {
2283
+ /** Only healthy / only broken feeds. Sent as `exact_search[health]`. */
2284
+ readonly health?: IntelSourceHealth;
2285
+ /** Only enabled, or only the ones that switched themselves off. */
2286
+ readonly enabled?: boolean;
2287
+ /** Every source driven by one script. */
2288
+ readonly scriptId?: Id;
2289
+ }
2290
+ /** Filters for {@link IntelScriptsNamespace.list}. */
2291
+ export interface ListIntelScriptsParams extends ContentListParams {
2292
+ /**
2293
+ * `true` for the platform scripts, `false` for yours. Omit for both - the
2294
+ * listing scope is `builtin OR mine`, so both populations are mixed by
2295
+ * default.
2296
+ */
2297
+ readonly builtin?: boolean;
2298
+ }
2299
+ /** Filters for {@link IntelItemsNamespace.list}. */
2300
+ export interface ListIntelItemsParams extends ContentListParams {
2301
+ /** Only items produced by one source. Sent as `exact_search[intel_source_id]`. */
2302
+ readonly sourceId?: Id;
2303
+ }
2304
+ /** Arguments for {@link IntelSourcesNamespace.create}. */
2305
+ export interface CreateIntelSourceInput {
2306
+ /** Up to 200 characters, and unique among YOUR sources - a clash is a 400. */
2307
+ readonly name: string;
2308
+ /**
2309
+ * The script that fetches it. Must be a built-in or one of yours;
2310
+ * `script_visible_to_owner` rejects anything else with
2311
+ * `400 "Intel script is not accessible"` rather than a 404, so this also
2312
+ * tells you the id exists. Do not use it as an existence oracle.
2313
+ */
2314
+ readonly intelScriptId: Id;
2315
+ /** Whatever that script reads. Free-form; the API validates nothing in it. */
2316
+ readonly config?: Record<string, Json>;
2317
+ /** 5-1440. Defaults to 15 server-side. */
2318
+ readonly pollIntervalMinutes?: number;
2319
+ /** Defaults to `true`. Create it disabled if you want to configure first. */
2320
+ readonly enabled?: boolean;
2321
+ }
2322
+ /**
2323
+ * Arguments for {@link IntelSourcesNamespace.update}.
2324
+ *
2325
+ * One key wider than the create form: `cursor` is updatable and not creatable.
2326
+ */
2327
+ export interface UpdateIntelSourceInput {
2328
+ readonly name?: string;
2329
+ readonly intelScriptId?: Id;
2330
+ /**
2331
+ * REPLACES the whole object; there is no merge. `assign_attributes` writes
2332
+ * the JSON column wholesale, so sending `{ url: "..." }` to a source that
2333
+ * also had a `selector` drops the selector. Read the source, spread, write.
2334
+ */
2335
+ readonly config?: Record<string, Json>;
2336
+ readonly pollIntervalMinutes?: number;
2337
+ /** Set back to `true` to revive a source that disabled itself. */
2338
+ readonly enabled?: boolean;
2339
+ /**
2340
+ * The incremental cursor. Set it to `null` to force the next poll to start
2341
+ * from the beginning - which for most scripts means re-fetching everything.
2342
+ *
2343
+ * `null` here is a JSON body `null`, not the query-string sentinel: bodies
2344
+ * never carry `\b`.
2345
+ */
2346
+ readonly cursor?: string | null;
2347
+ }
2348
+ /** Arguments for {@link IntelScriptsNamespace.create}. */
2349
+ export interface CreateIntelScriptInput {
2350
+ /** Up to 120 characters. */
2351
+ readonly name: string;
2352
+ /** The body. Up to {@link INTEL_SCRIPT_MAX_CODE_BYTES}. */
2353
+ readonly code: string;
2354
+ readonly description?: string;
2355
+ }
2356
+ /** Arguments for {@link IntelScriptsNamespace.update}. */
2357
+ export interface UpdateIntelScriptInput {
2358
+ readonly name?: string;
2359
+ readonly code?: string;
2360
+ readonly description?: string;
2361
+ }
2362
+ /**
2363
+ * Arguments for {@link IntelConfigNamespace.update}.
2364
+ *
2365
+ * Every key is optional and only the keys you send are written -
2366
+ * `assign_attributes` over a permitted hash - so this is a genuine partial
2367
+ * update, unlike {@link UpdateIntelSourceInput.config}.
2368
+ */
2369
+ export interface UpdateIntelConfigInput {
2370
+ readonly rubric?: string | null;
2371
+ /**
2372
+ * REPLACES the whole prompts object. Same trap as
2373
+ * {@link UpdateIntelSourceInput.config}: it is one JSON column, so a partial
2374
+ * object drops the keys you left out. Spread the current value.
2375
+ *
2376
+ * Only {@link INTEL_PROMPT_KEYS} are accepted; anything else fails the whole
2377
+ * request with a 400 naming the offenders.
2378
+ */
2379
+ readonly prompts?: Partial<Record<IntelPromptKey, string>>;
2380
+ readonly buildModel?: string | null;
2381
+ readonly reportModel?: string | null;
2382
+ /** 0-10. Outside the range is a 400, not a clamp. */
2383
+ readonly reportMinImportance?: number;
2384
+ /** 0-10. Outside the range is a 400, not a clamp. */
2385
+ readonly enrichMinImportance?: number;
2386
+ readonly webSearch?: boolean;
2387
+ /** 1-500. Outside the range is a 400, not a clamp. */
2388
+ readonly maxSources?: number;
2389
+ }
2390
+ /** What `POST /intel_sources/:id/run` answers with. The whole body. */
2391
+ export interface IntelSourceRunAccepted {
2392
+ /** Always `true`. The job was enqueued; nothing has been fetched yet. */
2393
+ readonly queued: boolean;
2394
+ }
2395
+ /**
2396
+ * `GET /intel_articles` and friends: the stories the pipeline built.
2397
+ *
2398
+ * Read-only plus a delete. There is no create and no update route -
2399
+ * `IntelArticle#creatable_by?` and `#updatable_by?` both return `false`
2400
+ * unconditionally, and the route is `only: [:index, :show, :destroy]`. Stories
2401
+ * come from `Intel::ArticleBuilder`, never from a client.
2402
+ */
2403
+ export declare class IntelArticlesNamespace extends Resource {
2404
+ /**
2405
+ * `GET /intel_articles` - your stories, most important first.
2406
+ *
2407
+ * Ordering is the controller's, not yours by default: `importance DESC,
2408
+ * last_seen_at DESC`, or `last_seen_at DESC` alone with `sort: "recent"`.
2409
+ * See {@link ListIntelArticlesParams.sort} for what happens when you pass
2410
+ * `order` as well - it is not what the names imply.
2411
+ *
2412
+ * Filter keys this controller declares for `search` / `exactSearch`:
2413
+ * `title`, `summary`, `category`, `importance`, `enriched`, plus the
2414
+ * inherited `id`, `created_at`, `updated_at`. Anything else is
2415
+ * `400 "Unknown search filter: x"` - fail-closed, never a wider result. The
2416
+ * free-text and importance filters are top-level instead: `q` and
2417
+ * `minImportance`.
2418
+ *
2419
+ * **Cost.** Every row runs its own `COUNT` for
2420
+ * {@link IntelArticle.n_sources}, because the blueprint calls
2421
+ * `intel_article_sources.size` and the index preloads nothing. Keep
2422
+ * `pageSize` in the tens, not at 500.
2423
+ *
2424
+ * The response carries an `ETag` and can answer `304` - except when
2425
+ * `random` is set, which short-circuits `resources_stale?`.
2426
+ *
2427
+ * @throws {OmsAuthError} 401 when anonymous.
2428
+ * @throws {OmsApiError} 403 `"Intel access is restricted."` for a signed-in
2429
+ * account outside the allowlist; 400 for an unrecognised filter key.
2430
+ */
2431
+ list(params?: ListIntelArticlesParams, options?: RequestOptions): Promise<Paginated<IntelArticle>>;
2432
+ /**
2433
+ * `GET /intel_articles/:id` - one story with its body, its sources, its
2434
+ * related stories and the reports that cited it.
2435
+ *
2436
+ * The `:extended` view, so it is a strict superset of the listing row. All
2437
+ * four extras are inlined without paging; see {@link IntelArticleDetail}.
2438
+ *
2439
+ * @throws {OmsApiError} 404 `"Resource not found"` when the id is not one of
2440
+ * yours - the lookup is `viewable_by(Current.user).find_by(id:)`, so
2441
+ * somebody else's story is indistinguishable from a typo, which is the
2442
+ * point.
2443
+ */
2444
+ get(id: Id, options?: RequestOptions): Promise<IntelArticleDetail>;
2445
+ /**
2446
+ * `DELETE /intel_articles/:id` - drops a story. `204`, empty body.
2447
+ *
2448
+ * The story's links to items are removed with it (`dependent: :destroy` on
2449
+ * `intel_article_sources`), but the {@link IntelItem} rows themselves SURVIVE
2450
+ * - they belong to the source, not to the story. They are also still marked
2451
+ * `processed_at`, so deleting a story does not make the pipeline rebuild it.
2452
+ * This is a hide, not an undo.
2453
+ *
2454
+ * @throws {OmsApiError} 404 when the story is not yours. 401
2455
+ * `"You are not authorized to destroy this resource"` cannot happen here -
2456
+ * `destroyable_by?` is `user == self.user` and the lookup already scoped it
2457
+ * - but note the API's habit of answering 401 rather than 403 for a failed
2458
+ * authorisation check, which the scripts routes DO hit.
2459
+ */
2460
+ delete(id: Id, options?: RequestOptions): Promise<void>;
2461
+ }
2462
+ /**
2463
+ * `GET /intel_reports` - the generated digests.
2464
+ *
2465
+ * Read-only plus a delete, for the same reason as the stories: reports come
2466
+ * from `Intel::GenerateReportJob`. There is no way to ask for one to be
2467
+ * generated over HTTP.
2468
+ */
2469
+ export declare class IntelReportsNamespace extends Resource {
2470
+ /**
2471
+ * `GET /intel_reports` - your reports, newest window first.
2472
+ *
2473
+ * `period_end DESC` is applied by the controller; as with the stories, a
2474
+ * `order` of your own becomes the PRIMARY key and this becomes the
2475
+ * tie-breaker.
2476
+ *
2477
+ * `kind` is the only declared filter beyond the inherited three. Use
2478
+ * {@link ListIntelReportsParams.kind}, which sends it as an exact match.
2479
+ *
2480
+ * @throws {OmsApiError} 403 `"Intel access is restricted."` outside the
2481
+ * allowlist.
2482
+ */
2483
+ list(params?: ListIntelReportsParams, options?: RequestOptions): Promise<Paginated<IntelReport>>;
2484
+ /**
2485
+ * `GET /intel_reports/:id` - the report with its body and its stories.
2486
+ *
2487
+ * @throws {OmsApiError} 404 when the report is not yours.
2488
+ */
2489
+ get(id: Id, options?: RequestOptions): Promise<IntelReportDetail>;
2490
+ /**
2491
+ * `DELETE /intel_reports/:id`. `204`, empty body.
2492
+ *
2493
+ * The stories it cited are untouched - only the join rows go.
2494
+ *
2495
+ * A deleted report can come back: `GenerateReportJob` is keyed by the unique
2496
+ * `(user, kind, period_end)` index, and deleting the row frees that key, so
2497
+ * the next dispatcher pass over the same window will regenerate it. Delete a
2498
+ * report to re-run it, not to suppress it.
2499
+ *
2500
+ * @throws {OmsApiError} 404 when the report is not yours.
2501
+ */
2502
+ delete(id: Id, options?: RequestOptions): Promise<void>;
2503
+ }
2504
+ /**
2505
+ * `/intel_sources` - the feeds you have configured. Full CRUD, plus a manual
2506
+ * run.
2507
+ */
2508
+ export declare class IntelSourcesNamespace extends Resource {
2509
+ /**
2510
+ * `GET /intel_sources` - your feeds.
2511
+ *
2512
+ * Declared filters: `name`, `health`, `enabled`, `intel_script_id`, plus the
2513
+ * inherited `id`, `created_at`, `updated_at`. The controller sets NO ordering
2514
+ * of its own, so a listing with no `order` is in whatever order Postgres
2515
+ * returns rows - which is not stable across pages. The SDK therefore sends
2516
+ * `created_at:desc` unless you say otherwise.
2517
+ *
2518
+ * A good health check in one call: `list({ health: "error" })`.
2519
+ *
2520
+ * @throws {OmsApiError} 403 outside the allowlist.
2521
+ */
2522
+ list(params?: ListIntelSourcesParams, options?: RequestOptions): Promise<Paginated<IntelSource>>;
2523
+ /**
2524
+ * `GET /intel_sources/:id`.
2525
+ *
2526
+ * `IntelSourceBlueprint` declares no `:extended` extras, so this is exactly
2527
+ * the shape a listing row has. Fetching one adds nothing but a round trip;
2528
+ * prefer finding it in {@link list} when you already have the page.
2529
+ *
2530
+ * @throws {OmsApiError} 404 when the source is not yours.
2531
+ */
2532
+ get(id: Id, options?: RequestOptions): Promise<IntelSource>;
2533
+ /**
2534
+ * `POST /intel_sources` - configures a feed. `201`.
2535
+ *
2536
+ * The source starts `health: "unknown"` and is not polled immediately: the
2537
+ * dispatcher picks it up on its next pass, or you can force it with
2538
+ * {@link run}.
2539
+ *
2540
+ * Three ways this fails with a 400 and a bare-string body:
2541
+ *
2542
+ * - `"Name has already been taken"` - names are unique per user;
2543
+ * - `"Intel script is not accessible"` - the script is neither a built-in nor
2544
+ * yours. This is a 400 rather than a 404, so it does not tell you whether
2545
+ * the id exists;
2546
+ * - `"Source limit reached (N)"` - you are at
2547
+ * {@link IntelConfig.max_sources}. Raise it with
2548
+ * {@link IntelConfigNamespace.update} if the ceiling is yours to raise.
2549
+ *
2550
+ * Not retried by default: a replayed `POST` after a lost response would fail
2551
+ * the uniqueness check rather than duplicate the row, but it would report
2552
+ * that failure as if the first attempt had never worked.
2553
+ */
2554
+ create(input: CreateIntelSourceInput, options?: RequestOptions): Promise<IntelSource>;
2555
+ /**
2556
+ * `PATCH /intel_sources/:id`.
2557
+ *
2558
+ * Note what is NOT writable: `health`, `last_error`, `last_run_at`,
2559
+ * `last_success_at` and `consecutive_failures` are not on `update_params`, so
2560
+ * you cannot clear a source's failure history by hand. Only a successful run
2561
+ * resets it (`register_success!`). Re-enabling a source that disabled itself
2562
+ * therefore leaves `consecutive_failures` at 20 until the next success - do
2563
+ * not read that field as "currently failing".
2564
+ *
2565
+ * {@link UpdateIntelSourceInput.config} replaces the whole object.
2566
+ *
2567
+ * @throws {OmsApiError} 404 when the source is not yours; 400 with the
2568
+ * validation sentence otherwise.
2569
+ */
2570
+ update(id: Id, input: UpdateIntelSourceInput, options?: RequestOptions): Promise<IntelSource>;
2571
+ /**
2572
+ * `DELETE /intel_sources/:id`. `204`, empty body.
2573
+ *
2574
+ * Destructive well beyond the row: `has_many :intel_items, dependent:
2575
+ * :destroy` takes every raw item this source ever produced, and the stories
2576
+ * built from them lose their citations
2577
+ * ({@link IntelArticleDetail.sources} shrinks, {@link IntelArticle.n_sources}
2578
+ * with it) while the stories themselves stay. Disabling is almost always what
2579
+ * you meant: `update(id, { enabled: false })`.
2580
+ *
2581
+ * @throws {OmsApiError} 404 when the source is not yours.
2582
+ */
2583
+ delete(id: Id, options?: RequestOptions): Promise<void>;
2584
+ /**
2585
+ * `POST /intel_sources/:id/run` - polls the source now instead of waiting
2586
+ * for its interval. `202 {"queued":true}`.
2587
+ *
2588
+ * **It enqueues; it does not fetch.** The answer arrives before anything has
2589
+ * happened, and it says nothing about whether the poll will succeed. To see
2590
+ * the outcome, re-read the source and watch {@link IntelSource.last_run_at},
2591
+ * {@link IntelSource.health} and {@link IntelSource.last_error}. There is no
2592
+ * job id and nothing to wait on.
2593
+ *
2594
+ * Three sharp edges:
2595
+ *
2596
+ * - it runs a source even when {@link IntelSource.enabled} is `false`. The
2597
+ * action does not look at the flag, so this is also how you test a feed you
2598
+ * have deliberately switched off;
2599
+ * - it is authorised by VISIBILITY only. The action does its own `find_by`
2600
+ * inside `viewable_by` and never calls `updatable_by?` - which happens to
2601
+ * be the same set here, since sources are only ever visible to their owner;
2602
+ * - it has **no bucket of its own**. It rides the general 600-per-minute
2603
+ * ceiling, so a loop can enqueue hundreds of `FetchSourceJob`s into the
2604
+ * `syncs` queue in seconds and starve everything else on it. Call it on a
2605
+ * user gesture; never in a poll loop.
2606
+ *
2607
+ * Not retried by default: a replay enqueues a second fetch.
2608
+ *
2609
+ * @throws {OmsApiError} 404 `"Resource not found"` when the source is not
2610
+ * yours.
2611
+ */
2612
+ run(id: Id, options?: RequestOptions): Promise<IntelSourceRunAccepted>;
2613
+ }
2614
+ /**
2615
+ * `/intel_scripts` - the fetchers. Full CRUD over YOUR scripts, read-only over
2616
+ * the platform's.
2617
+ */
2618
+ export declare class IntelScriptsNamespace extends Resource {
2619
+ /**
2620
+ * `GET /intel_scripts` - the built-ins plus yours, mixed.
2621
+ *
2622
+ * **No `code`.** The body is on the `:extended` view only, so every row here
2623
+ * has `code: undefined`. See {@link IntelScript.code}.
2624
+ *
2625
+ * Declared filters: `name`, `builtin`, `slug`, plus the inherited three.
2626
+ * The controller sets no ordering, so the SDK sends `created_at:desc`.
2627
+ *
2628
+ * @throws {OmsApiError} 403 outside the allowlist.
2629
+ */
2630
+ list(params?: ListIntelScriptsParams, options?: RequestOptions): Promise<Paginated<IntelScript>>;
2631
+ /**
2632
+ * `GET /intel_scripts/:id` - the script WITH its body.
2633
+ *
2634
+ * This is the only read that carries {@link IntelScript.code}. Works for a
2635
+ * built-in too: they are visible to everyone, so this is how you read one
2636
+ * before forking it.
2637
+ *
2638
+ * @throws {OmsApiError} 404 when the id is neither a built-in nor yours.
2639
+ */
2640
+ get(id: Id, options?: RequestOptions): Promise<IntelScript>;
2641
+ /**
2642
+ * `POST /intel_scripts` - saves a fetcher. `201`, with `code`.
2643
+ *
2644
+ * The controller transpiles the body in the `intel-runner` sidecar BEFORE
2645
+ * saving, so a syntax error surfaces here rather than at the first poll:
2646
+ * `400 "Invalid script: <the compiler's message>"`.
2647
+ *
2648
+ * **The check is best-effort and fails OPEN.** `check_script!` rescues
2649
+ * `Intel::RunnerClient::Error` and returns `nil`, so when the runner is down
2650
+ * or unreachable the script saves unchecked and a `201` means only "stored".
2651
+ * There is nothing on the response that distinguishes a checked save from an
2652
+ * unchecked one. Treat a successful create as "it parses, probably", and
2653
+ * confirm with {@link IntelSourcesNamespace.run} on a throwaway source.
2654
+ *
2655
+ * The check is a transpile, not an execution: it proves the code parses, not
2656
+ * that it fetches anything.
2657
+ *
2658
+ * The created script is always yours - `builtin` is not on `create_params`,
2659
+ * so it cannot be set - and up to
2660
+ * {@link INTEL_SCRIPT_MAX_CODE_BYTES} long.
2661
+ *
2662
+ * Not retried by default: a replay creates a second script.
2663
+ */
2664
+ create(input: CreateIntelScriptInput, options?: RequestOptions): Promise<IntelScript>;
2665
+ /**
2666
+ * `PATCH /intel_scripts/:id` - edits one of YOUR scripts. Answers with `code`.
2667
+ *
2668
+ * Same best-effort transpile check as {@link create}, and only when `code` is
2669
+ * present in the body.
2670
+ *
2671
+ * **A built-in answers `401`, not `403`.** `IntelScript#updatable_by?`
2672
+ * requires `!builtin?`, and `CrudActions#update` reports a failed
2673
+ * authorisation with `unauthorized!` - so the body is
2674
+ * `"You are not authorized to update this resource"` under a 401 status. That
2675
+ * is an authorisation refusal wearing an authentication status code: do NOT
2676
+ * let a generic 401 handler log the user out over it. Check
2677
+ * {@link IntelScript.builtin} first and fork instead of editing.
2678
+ *
2679
+ * A live edit takes effect on the next poll of every source using this
2680
+ * script; there is no versioning and no rollback.
2681
+ *
2682
+ * @throws {OmsApiError} 404 when the id is not visible to you; 401 for a
2683
+ * built-in; 400 for a syntax error or an over-long body.
2684
+ */
2685
+ update(id: Id, input: UpdateIntelScriptInput, options?: RequestOptions): Promise<IntelScript>;
2686
+ /**
2687
+ * `DELETE /intel_scripts/:id`. `204`, empty body.
2688
+ *
2689
+ * Refuses while any source still uses it: `has_many :intel_sources,
2690
+ * dependent: :restrict_with_error` turns the destroy into a validation
2691
+ * failure, which `CrudActions#destroy` reports as
2692
+ * `400 "Cannot delete record because dependent intel sources exist"`. Delete
2693
+ * or repoint the sources first - {@link IntelSourcesNamespace.list} with
2694
+ * `scriptId` finds them in one call.
2695
+ *
2696
+ * A built-in answers `401` with `"You are not authorized to destroy this
2697
+ * resource"`, for the reason spelled out on {@link update}.
2698
+ */
2699
+ delete(id: Id, options?: RequestOptions): Promise<void>;
2700
+ }
2701
+ /**
2702
+ * `/intel_items` - the raw material.
2703
+ *
2704
+ * Read-only plus a delete: `creatable_by?` and `updatable_by?` are hard `false`
2705
+ * and the route is `only: [:index, :show, :destroy]`. Items are written by
2706
+ * `Intel::FetchSourceJob` and by nothing else.
2707
+ *
2708
+ * The web frontend never touches this family. It is here because the stories
2709
+ * only carry a citation stub ({@link IntelArticleSourceRef}) and this is the
2710
+ * only way to read the body behind one.
2711
+ */
2712
+ export declare class IntelItemsNamespace extends Resource {
2713
+ /**
2714
+ * `GET /intel_items` - raw items, newest first.
2715
+ *
2716
+ * **Heavy.** Every row carries {@link IntelItem.content} in full - the whole
2717
+ * article text a script scraped - and there is no lighter view. The SDK
2718
+ * defaults to a page of 25 for that reason; raising it is how you get a
2719
+ * multi-megabyte response.
2720
+ *
2721
+ * Declared filters: `intel_source_id`, `external_id`, `title`, `content`,
2722
+ * `url`, plus the inherited three. Note `processed_at` is NOT among them and
2723
+ * is not on the blueprint either, so there is no way to list only the
2724
+ * unprocessed items - {@link IntelStats.totals.pending_items} is the only
2725
+ * window onto that backlog.
2726
+ *
2727
+ * The controller sets no ordering; the SDK sends `created_at:desc`.
2728
+ */
2729
+ list(params?: ListIntelItemsParams, options?: RequestOptions): Promise<Paginated<IntelItem>>;
2730
+ /**
2731
+ * `GET /intel_items/:id` - one raw item.
2732
+ *
2733
+ * The blueprint has no `:extended` extras, so this is the same shape a
2734
+ * listing row has. Use it to expand one {@link IntelArticleSourceRef} without
2735
+ * pulling a page of bodies.
2736
+ *
2737
+ * @throws {OmsApiError} 404 when the item is not yours.
2738
+ */
2739
+ get(id: Id, options?: RequestOptions): Promise<IntelItem>;
2740
+ /**
2741
+ * `DELETE /intel_items/:id`. `204`, empty body.
2742
+ *
2743
+ * Rarely what you want. The item's `external_id` uniqueness is what stops the
2744
+ * next poll re-fetching it, so deleting one invites it straight back on the
2745
+ * following run - and if the story built from it survives, you get a second
2746
+ * citation of the same thing. Delete the SOURCE, or leave items alone.
2747
+ *
2748
+ * @throws {OmsApiError} 404 when the item is not yours.
2749
+ */
2750
+ delete(id: Id, options?: RequestOptions): Promise<void>;
2751
+ }
2752
+ /**
2753
+ * `/intel_config` - the per-user pipeline settings.
2754
+ *
2755
+ * A Rails SINGULAR resource (`resource :intel_config`), so the path has no id
2756
+ * and there is no listing: `GET /intel_config` and `PATCH /intel_config` are
2757
+ * the whole surface. Both act on the caller's own row and there is no way to
2758
+ * address anybody else's.
2759
+ */
2760
+ export declare class IntelConfigNamespace extends Resource {
2761
+ /**
2762
+ * `GET /intel_config` - your settings.
2763
+ *
2764
+ * **This read WRITES.** The controller calls `IntelConfig.for(Current.user)`,
2765
+ * which is `find_or_create_by!`, so a first call inserts the row with the
2766
+ * column defaults and returns it. Consequences worth knowing: it is not safe
2767
+ * to fire at high frequency (two concurrent first calls race on the unique
2768
+ * index and one raises), the response is a `200` even when it just created
2769
+ * something, and `created_at` on a "read" can be now.
2770
+ *
2771
+ * @throws {OmsApiError} 403 `"Intel access is restricted."` outside the
2772
+ * allowlist - checked before the row is created, so a refused caller does
2773
+ * not leave a row behind.
2774
+ */
2775
+ get(options?: RequestOptions): Promise<IntelConfig>;
2776
+ /**
2777
+ * `PATCH /intel_config` - changes settings. Answers with the whole row.
2778
+ *
2779
+ * A genuine partial update for the scalar fields, and a whole-object replace
2780
+ * for `prompts` - see {@link UpdateIntelConfigInput.prompts}.
2781
+ *
2782
+ * The route also accepts `PUT`, and it means exactly the same thing: Rails
2783
+ * maps both onto `update` and the controller does not read the verb. There is
2784
+ * no "replace the whole config" call.
2785
+ *
2786
+ * Failures are a `400` whose body is ONE sentence, not a field map:
2787
+ * `ApplicationRecord#error_messages` is `errors.full_messages.to_sentence`,
2788
+ * so several violations arrive joined by commas and "and". Parse it for
2789
+ * humans, not for code.
2790
+ *
2791
+ * @throws {OmsApiError} 400 for a threshold outside `0..10`, a `max_sources`
2792
+ * outside `1..500`, or a `prompts` key outside {@link INTEL_PROMPT_KEYS}.
2793
+ */
2794
+ update(input: UpdateIntelConfigInput, options?: RequestOptions): Promise<IntelConfig>;
2795
+ }
2796
+ /** `/intel_stats` - the dashboard counters. One route, one verb. */
2797
+ export declare class IntelStatsNamespace extends Resource {
2798
+ /**
2799
+ * `GET /intel_stats` - every counter the intel dashboard shows, in one call.
2800
+ *
2801
+ * Also a Rails singular resource, so the path is `/intel_stats` with no id
2802
+ * despite the plural spelling.
2803
+ *
2804
+ * **Cost, and the reason not to poll this.** The controller does
2805
+ * `articles.pluck(:importance)` - it loads the importance of EVERY story you
2806
+ * own into Ruby memory to build {@link IntelStats.by_importance} - and then
2807
+ * runs five more aggregate queries beside it. There is no cache, no `ETag`
2808
+ * (the hand-written action never calls `stale?`, unlike every list in this
2809
+ * file) and therefore no `304`. Cost grows linearly with your story count for
2810
+ * ever. Fetch it on a dashboard open, not on a timer.
2811
+ *
2812
+ * Read {@link IntelStatsTotals.sources} before you label it: it does not
2813
+ * count your feeds.
2814
+ *
2815
+ * @throws {OmsApiError} 403 `"Intel access is restricted."` outside the
2816
+ * allowlist.
2817
+ */
2818
+ get(options?: RequestOptions): Promise<IntelStats>;
2819
+ }
2820
+ /**
2821
+ * Base of the third-party image proxy {@link intelArticleImageUrl} builds on.
2822
+ *
2823
+ * `wsrv.nl` is a free public image CDN. It is NOT this API and NOT our
2824
+ * infrastructure.
2825
+ */
2826
+ export declare const INTEL_IMAGE_PROXY_BASE_URL = "https://wsrv.nl/";
2827
+ /** Knobs for {@link intelArticleImageUrl}. The defaults are the web app's. */
2828
+ export interface IntelImageOptions {
2829
+ /** Target width in pixels. Default 480. Height follows the aspect ratio. */
2830
+ readonly width?: number;
2831
+ /** Quality, 1-100. Default 45 - low on purpose; these are thumbnails. */
2832
+ readonly quality?: number;
2833
+ }
2834
+ /**
2835
+ * Builds a resized, re-compressed URL for {@link IntelArticle.image_url}.
2836
+ *
2837
+ * Pure string building, no request, isolate-safe. Returns `""` for a story with
2838
+ * no image so it can be dropped straight into an `<img src>` without a
2839
+ * conditional - though a real client should test the field and render nothing.
2840
+ *
2841
+ * ## What this actually does, and why you might not want it
2842
+ *
2843
+ * `image_url` is the raw `og:image` of a news site: full size, arbitrary
2844
+ * format, arbitrary weight, and served from that site's own host. Nothing in
2845
+ * this API resizes it. The web frontend's answer is to route it through
2846
+ * `wsrv.nl`, a free public image CDN, which fetches the origin image and hands
2847
+ * back a width-limited WebP.
2848
+ *
2849
+ * The trade is explicit and it is not the SDK's to make silently:
2850
+ *
2851
+ * - the ORIGIN URL is sent to a third party in a query string, so wsrv.nl
2852
+ * learns which article your user is looking at, and so does anyone reading
2853
+ * the request. There is no credential involved - the images are public - but
2854
+ * it is still a referrer-shaped leak;
2855
+ * - availability is theirs, not ours. A wsrv.nl outage is a page of broken
2856
+ * images, and there is no fallback in the URL;
2857
+ * - `&we` asks it not to enlarge images smaller than `width`.
2858
+ *
2859
+ * If neither trade suits you, use {@link IntelArticle.image_url} directly and
2860
+ * size it in CSS. This helper exists because the web app cannot drop its own
2861
+ * intel service without it, and it is ported here rather than reinvented.
2862
+ */
2863
+ export declare function intelArticleImageUrl(imageUrl: string | null | undefined, options?: IntelImageOptions): string;
2864
+ /**
2865
+ * The `intel` namespace, reachable as `oms.content.intel`.
2866
+ *
2867
+ * Seven typed families over the backend's own tables, plus {@link proxy} for
2868
+ * the untyped passthrough to the old sidecar. The three proxy methods are also
2869
+ * mirrored on this class so that code written against 0.3.0's
2870
+ * `oms.content.intel.get(path)` keeps working; new code should say
2871
+ * `oms.content.intel.proxy.get(path)`, which cannot be confused with
2872
+ * {@link IntelArticlesNamespace.get}.
2873
+ *
2874
+ * A tour of the data model, because the names do not give it away:
2875
+ *
2876
+ * 1. a {@link IntelScript} knows HOW to fetch one kind of feed;
2877
+ * 2. an {@link IntelSource} is that script plus its settings - a feed you
2878
+ * actually follow;
2879
+ * 3. polling a source writes {@link IntelItem} rows: raw, unprocessed, one per
2880
+ * thing the feed published;
2881
+ * 4. the analysis pipeline groups items into {@link IntelArticle} stories,
2882
+ * scores them against your {@link IntelConfig} rubric, enriches the
2883
+ * important ones and links related ones together;
2884
+ * 5. {@link IntelReport} digests summarise a closed time window of stories.
2885
+ *
2886
+ * Only steps 1 and 2 are yours to write. Everything from step 3 on is produced
2887
+ * by background jobs and is read-only over HTTP - a delete is the only mutation
2888
+ * you get, and it is a hide, not an undo.
2889
+ */
2890
+ export declare class IntelNamespace extends Resource {
2891
+ /** Stories: the analysed, grouped, scored output. Read plus delete. */
2892
+ readonly articles: IntelArticlesNamespace;
2893
+ /** Generated digests over closed time windows. Read plus delete. */
2894
+ readonly reports: IntelReportsNamespace;
2895
+ /** The feeds you follow. Full CRUD, plus a manual run. */
2896
+ readonly sources: IntelSourcesNamespace;
2897
+ /** The fetchers. Full CRUD over yours; the built-ins are read-only. */
2898
+ readonly scripts: IntelScriptsNamespace;
2899
+ /** The raw material behind the stories. Read plus delete. */
2900
+ readonly items: IntelItemsNamespace;
2901
+ /** Your rubric, thresholds and prompt overrides. */
2902
+ readonly config: IntelConfigNamespace;
2903
+ /** Dashboard counters, in one expensive call. */
2904
+ readonly stats: IntelStatsNamespace;
2905
+ /**
2906
+ * The untyped passthrough to the old intel sidecar.
2907
+ *
2908
+ * @deprecated See {@link IntelProxyNamespace}. Kept only because the route
2909
+ * still answers; the typed families above are the intel API now.
2910
+ */
2911
+ readonly proxy: IntelProxyNamespace;
2912
+ constructor(http: ApiClient);
2913
+ /**
2914
+ * Alias for {@link IntelProxyNamespace.get}. Kept so 0.3.0 call sites still
2915
+ * compile; prefer `oms.content.intel.proxy.get(path)`.
2916
+ *
2917
+ * @deprecated The intel sidecar is being retired: intel now lives entirely
2918
+ * inside Rails. See {@link IntelProxyNamespace}.
2919
+ */
2920
+ get<T = unknown>(path: string, query?: QueryParams, options?: RequestOptions): Promise<T>;
2921
+ /**
2922
+ * Alias for {@link IntelProxyNamespace.fetch}. Kept so 0.3.0 call sites still
2923
+ * compile; prefer `oms.content.intel.proxy.fetch(path)`.
2924
+ *
2925
+ * @deprecated The intel sidecar is being retired: intel now lives entirely
2926
+ * inside Rails. See {@link IntelProxyNamespace}.
2927
+ */
2928
+ fetch(path: string, query?: QueryParams, options?: RequestOptions): Promise<FileOutput>;
2929
+ /**
2930
+ * Alias for {@link IntelProxyNamespace.url}. Kept so 0.3.0 call sites still
2931
+ * compile; prefer `oms.content.intel.proxy.url(path)`.
2932
+ *
2933
+ * @deprecated The intel sidecar is being retired: intel now lives entirely
2934
+ * inside Rails. See {@link IntelProxyNamespace}.
2935
+ */
2936
+ url(path: string, query?: QueryParams): string;
2937
+ }
2938
+ /**
2939
+ * The `content` namespace, reachable as `oms.content`.
2940
+ *
2941
+ * An umbrella over ten unrelated corners of the API. Nothing is shared between
2942
+ * them, so mount the sub-namespaces directly if a flatter surface reads better
2943
+ * - each one is exported on its own.
2944
+ */
2945
+ export declare class ContentNamespace extends Resource {
2946
+ /** Blogs, blog posts and subscriptions. `.posts` hangs off it. */
2947
+ readonly blogs: BlogsNamespace;
2948
+ /** The notification inbox. HTTP half only; the cable pushes the rest. */
2949
+ readonly notifications: NotificationsNamespace;
2950
+ /** The feedback box, and its admin queue. */
2951
+ readonly feedbacks: FeedbacksNamespace;
2952
+ /** The joke table behind the loading screens. */
2953
+ readonly jokes: JokesNamespace;
2954
+ /** The public config blob a client reads before it has a credential. */
2955
+ readonly config: SiteConfigNamespace;
2956
+ /** The status page: live probes and the 90-day uptime report. */
2957
+ readonly status: ServicesStatusNamespace;
2958
+ /** Per-user "which parts of the site do you open" counters. */
2959
+ readonly serviceUsages: ServiceUsagesNamespace;
2960
+ /** Two admin storage reports. */
2961
+ readonly analysis: AnalysisNamespace;
2962
+ /** The Space Invaders leaderboard. */
2963
+ readonly spaceInvaders: SpaceInvadersNamespace;
2964
+ /**
2965
+ * Intel: seven typed families over the backend's own tables, with the
2966
+ * untyped sidecar passthrough kept on `.proxy`. See {@link IntelNamespace}.
2967
+ */
2968
+ readonly intel: IntelNamespace;
2969
+ constructor(http: ApiClient);
2970
+ }