@omelhorsite/sdk 0.2.0 → 0.4.0

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