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