@owncast/plugin-sdk 0.6.0 → 0.11.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/index.d.ts CHANGED
@@ -1,34 +1,25 @@
1
1
  /**
2
2
  * Built-in chat message payload (`chat.message.received` and the chat filter).
3
3
  *
4
- * `user` carries the full sender identity use `user.id` for stable per-user
4
+ * `user` carries the full sender identity. Use `user.id` for stable per-user
5
5
  * state and `user.scopes` (e.g. `"MODERATOR"`) for reliable, non-spoofable
6
6
  * moderation gating rather than matching on the display name. `clientId`
7
- * identifies the originating connection; pass it to `owncast.chat.sendTo` (or
7
+ * identifies the originating connection. Pass it to `owncast.chat.sendTo` (or
8
8
  * `owncast.chat.replyTo(msg, …)`) to whisper a reply back to the sender.
9
9
  *
10
10
  * `user` is undefined for the rare message with no associated account.
11
11
  */
12
12
  export interface ChatMessage {
13
13
  id: string;
14
- user?: ChatUser;
14
+ user?: User;
15
15
  clientId?: number;
16
16
  body: string;
17
17
  timestamp: string;
18
18
  }
19
19
 
20
- /** A chat user, payload of join/part/rename events. */
21
- export interface ChatUser {
22
- id: string;
23
- displayName: string;
24
- isBot?: boolean;
25
- isAuthenticated?: boolean;
26
- scopes?: string[];
27
- }
28
-
29
20
  /** Payload of `chat.user.renamed`, the same user changing their name. */
30
21
  export interface ChatUserRename {
31
- user: ChatUser;
22
+ user: User;
32
23
  previousName: string;
33
24
  }
34
25
 
@@ -36,7 +27,7 @@ export interface ChatUserRename {
36
27
  export interface ChatMessageModeration {
37
28
  messageId: string;
38
29
  visible: boolean;
39
- moderator?: ChatUser;
30
+ moderator?: User;
40
31
  }
41
32
 
42
33
  /** Stream-lifecycle payloads. */
@@ -80,20 +71,36 @@ export interface StreamBroadcaster {
80
71
  bitrates?: number[];
81
72
  }
82
73
 
74
+ /** Viewer autoplay behavior. */
75
+ export type AutoplayMode = "off" | "always" | "sound-only";
76
+
77
+ /** H.264 encoder names accepted by Owncast video config writes.
78
+ * Hardware encoders must also be available in ffmpeg. */
79
+ export type VideoCodec =
80
+ | "libx264"
81
+ | "h264_omx"
82
+ | "h264_vaapi"
83
+ | "h264_qsv"
84
+ | "h264_nvenc"
85
+ | "h264_v4l2m2m"
86
+ | "h264_videotoolbox";
87
+
83
88
  /** One configured output rendition, part of VideoConfig (owncast.videoConfig). */
84
89
  export interface StreamVariant {
85
90
  width: number;
86
91
  height: number;
87
92
  framerate: number;
88
93
  videoBitrate: number;
89
- audioBitrate: number;
94
+ cpuUsageLevel: number;
90
95
  isPassthrough: boolean;
91
96
  }
92
97
 
93
98
  /** The current video/transcoding config returned by owncast.videoConfig.read(). */
94
99
  export interface VideoConfig {
95
100
  latencyLevel: number;
101
+ /** The configured encoder. Reads may report a legacy or newer host value. */
96
102
  codec: string;
103
+ autoplay: AutoplayMode;
97
104
  variants: StreamVariant[];
98
105
  }
99
106
 
@@ -101,7 +108,8 @@ export interface VideoConfig {
101
108
  * are left unchanged. */
102
109
  export interface VideoConfigUpdate {
103
110
  latencyLevel?: number;
104
- codec?: string;
111
+ codec?: VideoCodec;
112
+ autoplay?: AutoplayMode;
105
113
  variants?: StreamVariant[];
106
114
  }
107
115
 
@@ -129,9 +137,11 @@ export const Events: {
129
137
  readonly SseConnect: "sse.connect";
130
138
  readonly SseDisconnect: "sse.disconnect";
131
139
  readonly Tick: "tick";
140
+ readonly FediverseActivity: "fediverse.activity";
132
141
  readonly FediverseFollow: "fediverse.follow";
133
142
  readonly FediverseLike: "fediverse.like";
134
143
  readonly FediverseRepost: "fediverse.repost";
144
+ readonly FediverseQuote: "fediverse.quote";
135
145
  readonly FediverseMention: "fediverse.mention";
136
146
  readonly FediverseReply: "fediverse.reply";
137
147
  };
@@ -146,10 +156,31 @@ export interface FediverseActor {
146
156
 
147
157
  export interface FediverseEngagement {
148
158
  actor: FediverseActor;
149
- /** For likes and reposts: the target object URL. Not set for follows. */
159
+ /** For likes, reposts, and quotes: the target object URL. Not set for follows. */
150
160
  target?: { url: string };
151
161
  }
152
162
 
163
+ export interface FediverseTargetedEngagement extends FediverseEngagement {
164
+ target: { url: string };
165
+ }
166
+
167
+ /** An accepted quote request. `target` identifies the local post being quoted,
168
+ * while `url` identifies the remote quote post. Content metadata is present
169
+ * when the requesting server embeds the quote Note in its request. */
170
+ export interface FediverseQuote extends FediverseTargetedEngagement {
171
+ content?: string; // HTML from the source instance
172
+ contentText?: string; // HTML stripped to plain text
173
+ url: string; // permalink to the remote quote post
174
+ postedAt?: string; // ISO-8601
175
+ inReplyTo?: string;
176
+ attachments?: {
177
+ url: string;
178
+ mediaType: string;
179
+ alt?: string;
180
+ }[];
181
+ language?: string;
182
+ }
183
+
153
184
  /** Inbound fediverse post, a mention or reply that contains content the
154
185
  * plugin can act on. Carries both the rendered content (which has the
155
186
  * source instance's HTML) and a plain-text version (HTML stripped). */
@@ -176,6 +207,7 @@ export const Permissions: {
176
207
  readonly StorageKV: "storage.kv";
177
208
  readonly StorageUpload: "storage.upload";
178
209
  readonly StorageFS: "storage.fs";
210
+ readonly StorageSQL: "storage.sql";
179
211
  readonly EventsEmit: "events.emit";
180
212
  readonly NetworkFetch: "network.fetch";
181
213
  readonly HttpServe: "http.serve";
@@ -183,13 +215,45 @@ export const Permissions: {
183
215
  readonly NotificationsSend: "notifications.send";
184
216
  readonly UsersRead: "users.read";
185
217
  readonly UsersModerate: "users.moderate";
218
+ readonly UsersRegister: "users.register";
219
+ readonly AuthGate: "auth.gate";
186
220
  readonly FediversePost: "fediverse.post";
221
+ readonly FediverseInbound: "fediverse.inbound";
187
222
  readonly HttpSSE: "http.sse";
188
223
  readonly VideoConfigRead: "videoconfig.read";
189
224
  readonly VideoConfigWrite: "videoconfig.write";
190
225
  readonly UIModify: "ui.modify";
191
226
  };
192
227
 
228
+ /** Request for `owncast.users.register`. */
229
+ export interface UserRegisterRequest {
230
+ /** Stable external identity within this plugin's provider namespace. */
231
+ authId: string;
232
+ /** Optional display name to seed on the user. Omit or pass `null` to generate one. */
233
+ displayName?: string | null;
234
+ /** Optional scopes to grant the user (e.g. `["MODERATOR"]`). */
235
+ scopes?: string[];
236
+ /** Verified public profile URL. The host accepts only absolute HTTP(S) URLs. */
237
+ profileUrl?: string;
238
+ /** Label for the verified identity, such as a GitHub login or fediverse handle. */
239
+ handle?: string;
240
+ /** Surface the verified identity publicly only when the viewer opted in. */
241
+ public?: boolean;
242
+ }
243
+
244
+ /** Result of `owncast.users.register`: the resolved Owncast user ID. */
245
+ export interface UserRegisterResult {
246
+ userId: string;
247
+ }
248
+
249
+ /** Request for `owncast.auth.grantSession`. */
250
+ export interface GrantSessionRequest {
251
+ /** The Owncast user ID returned by `owncast.users.register`. */
252
+ userId: string;
253
+ /** Optional session lifetime in seconds. 0/omitted uses the host default. */
254
+ ttl?: number;
255
+ }
256
+
193
257
  export interface BrowserPushPayload {
194
258
  title: string;
195
259
  body?: string;
@@ -221,10 +285,14 @@ export interface FederationInfo {
221
285
  isPrivate?: boolean;
222
286
  }
223
287
 
224
- /** A user record from owncast.users.list() / .get(). */
288
+ /** A user. The sender identity carried by every chat payload
289
+ * (chat.message.received, join/part/rename, moderation) and the record
290
+ * returned by owncast.users.list() / .get(). `displayColor` is an index into
291
+ * the instance's configured user-color palette, not a literal color. */
225
292
  export interface User {
226
293
  id: string;
227
294
  displayName: string;
295
+ displayColor: number;
228
296
  previousNames?: string[];
229
297
  createdAt?: string;
230
298
  disabledAt?: string; // ISO-8601 if banned, omitted otherwise
@@ -249,19 +317,68 @@ export interface UploadResult {
249
317
  url: string;
250
318
  }
251
319
 
252
- /** Result of a mutating owncast.fs call (write/delete). `ok` is false and
253
- * `error` is set when the host rejected the operation. */
320
+ /** Result of a mutating owncast.fs call (write/delete). An empty object means
321
+ * success. `error` is set when the host rejected the operation. */
254
322
  export interface FsResult {
255
- ok: boolean;
256
323
  error?: string;
257
324
  }
258
325
 
326
+ /** A value a plugin can bind to a statement parameter, or read back out of a
327
+ * column. Blobs arrive base64-encoded as strings. */
328
+ export type SQLValue = null | boolean | number | string;
329
+
330
+ /** Result of `owncast.sql.exec`. Absence of `error` means success. Both
331
+ * counters are SQLite 64-bit integers, so they lose precision in JavaScript
332
+ * above `Number.MAX_SAFE_INTEGER`. */
333
+ export interface SQLExecResult {
334
+ error?: string;
335
+ rowsAffected: number;
336
+ lastInsertId: number;
337
+ }
338
+
339
+ /** One row as `owncast.sql.query` hands it back: column name to value. */
340
+ export type SQLRow = Record<string, SQLValue>;
341
+
342
+ /** The host's raw query response, before the SDK keys rows by column name.
343
+ * Absence of `error` means success. `rows` holds values in `columns` order,
344
+ * and `truncated` is set when more rows matched than the caller's row limit. */
345
+ export interface SQLQueryResult {
346
+ error?: string;
347
+ columns: string[];
348
+ rows: SQLValue[][];
349
+ truncated?: boolean;
350
+ }
351
+
259
352
  export const filter: {
260
353
  pass(): FilterResult;
261
354
  modify(payload: any): FilterResult;
262
355
  drop(reason?: string): FilterResult;
263
356
  };
264
357
 
358
+ /** Request passed to `onAuthCheck`: the host-resolved identity of the viewer
359
+ * whose session is being re-validated (same `user` shape `onHttpRequest`
360
+ * receives, and the plugin never re-resolves it). */
361
+ export interface AuthCheckRequest {
362
+ user: User;
363
+ }
364
+
365
+ /** Verdict returned from `onAuthCheck`:
366
+ * - `ok` keep the session as-is
367
+ * - `refresh` keep it and re-issue the cookie (optionally with a new `ttl`
368
+ * in seconds) for sliding-expiry
369
+ * - `deny` end the session and bounce the viewer back to the login screen */
370
+ export type AuthCheckResult =
371
+ | { action: "ok" }
372
+ | { action: "refresh"; ttl?: number }
373
+ | { action: "deny"; reason?: string };
374
+
375
+ /** Verdict helpers for `onAuthCheck`. */
376
+ export const authCheck: {
377
+ ok(): AuthCheckResult;
378
+ refresh(opts?: { ttl?: number }): AuthCheckResult;
379
+ deny(reason?: string): AuthCheckResult;
380
+ };
381
+
265
382
  /** Incoming HTTP request, paths are relative to the plugin's namespace
266
383
  * (i.e. the leading /plugins/<name>/ has been stripped). */
267
384
  export interface IncomingHttpRequest {
@@ -275,7 +392,7 @@ export interface IncomingHttpRequest {
275
392
  authenticated: boolean;
276
393
  /** Identity of the user that made the request, when it came with a
277
394
  * user-token. Undefined for anonymous or admin-only requests. */
278
- user?: ChatUser;
395
+ user?: User;
279
396
  }
280
397
 
281
398
  export interface OutgoingHttpResponse {
@@ -290,7 +407,7 @@ export interface ContentRequest {
290
407
  slug: string;
291
408
  /** The viewing user's chat identity, when available. Undefined for
292
409
  * anonymous viewers or when the host cannot resolve an identity. */
293
- user?: ChatUser;
410
+ user?: User;
294
411
  }
295
412
 
296
413
  /** Payload for the sse.connect / sse.disconnect events. Fired when a browser
@@ -302,7 +419,7 @@ export interface ContentRequest {
302
419
  export interface SSEConnectionEvent {
303
420
  channel: string;
304
421
  connectionId: number;
305
- user?: ChatUser;
422
+ user?: User;
306
423
  }
307
424
 
308
425
  /** Payload for the once-a-second tick event (onTick). `now` is the host
@@ -312,21 +429,13 @@ export interface TickEvent {
312
429
  }
313
430
 
314
431
  export interface PluginDef {
315
- /** Declarative chat-command table. When set, the SDK wires the chat
316
- * subscription and prefix parsing for you no onChatMessage needed. Maps
317
- * canonical command name → definition (run/description/usage/aliases/
318
- * modOnly/cooldownMs/...); see {@link CommandDefinition}. For advanced
319
- * composition (e.g. dropping command messages via a filter) use the
320
- * lower-level {@link defineCommands} router instead. If you also provide
321
- * onChatMessage, the router runs first and then onChatMessage runs for every
322
- * message. */
432
+ /** Declarative chat commands with aliases, moderator gates, and per-user
433
+ * cooldowns. Command messages also remain available to onChatMessage. */
323
434
  commands?: Record<string, CommandDefinition>;
324
435
  /** Command prefix for the `commands` table. Default "!". */
325
436
  commandPrefix?: string;
326
437
  /** Match command names case-sensitively. Default false. */
327
438
  commandsCaseSensitive?: boolean;
328
- /** Called when a prefixed message matched no command in `commands`. */
329
- onUnknownCommand?(ctx: CommandContext): void;
330
439
 
331
440
  /** Notification handler for chat messages. Fire-and-forget. */
332
441
  onChatMessage?(msg: ChatMessage): void | Promise<void>;
@@ -336,9 +445,9 @@ export interface PluginDef {
336
445
  filterChatMessage?(msg: ChatMessage): FilterResult;
337
446
 
338
447
  /** User connected to chat. */
339
- onChatUserJoined?(user: ChatUser): void | Promise<void>;
448
+ onChatUserJoined?(user: User): void | Promise<void>;
340
449
  /** User disconnected from chat. */
341
- onChatUserParted?(user: ChatUser): void | Promise<void>;
450
+ onChatUserParted?(user: User): void | Promise<void>;
342
451
  /** User changed their display name. */
343
452
  onChatUserRenamed?(change: ChatUserRename): void | Promise<void>;
344
453
  /** A chat message was hidden or restored by a moderator. */
@@ -362,15 +471,20 @@ export interface PluginDef {
362
471
  * in unix milliseconds. Defining this opts the plugin into the tick. */
363
472
  onTick?(event: TickEvent): void | Promise<void>;
364
473
 
365
- /** Someone on the fediverse followed the streamer's account. */
474
+ /** A verified inbound ActivityPub activity as its raw JSON object. Requires `fediverse.inbound`. */
475
+ onFediverse?(activity: Record<string, unknown>): void | Promise<void>;
476
+
477
+ /** Someone on the fediverse followed the streamer's account. Requires `fediverse.inbound`. */
366
478
  onFediverseFollow?(event: FediverseEngagement): void | Promise<void>;
367
- /** Someone on the fediverse liked a streamer post / federated stream announcement. */
368
- onFediverseLike?(event: FediverseEngagement): void | Promise<void>;
369
- /** Someone on the fediverse boosted (reposted) a streamer post. */
370
- onFediverseRepost?(event: FediverseEngagement): void | Promise<void>;
371
- /** Someone @-mentioned the streamer in a public post. */
479
+ /** Someone on the fediverse liked a streamer post / federated stream announcement. Requires `fediverse.inbound`. */
480
+ onFediverseLike?(event: FediverseTargetedEngagement): void | Promise<void>;
481
+ /** Someone on the fediverse boosted (reposted) a streamer post. Requires `fediverse.inbound`. */
482
+ onFediverseRepost?(event: FediverseTargetedEngagement): void | Promise<void>;
483
+ /** Someone on the fediverse quoted a locally authored post. `target.url` identifies the local post and `url` identifies the remote quote post. Requires `fediverse.inbound`. */
484
+ onFediverseQuote?(event: FediverseQuote): void | Promise<void>;
485
+ /** Someone @-mentioned the streamer in a public post. Requires `fediverse.inbound`. */
372
486
  onFediverseMention?(post: FediverseInboundPost): void | Promise<void>;
373
- /** Someone replied to one of the streamer's federated posts. */
487
+ /** Someone replied to one of the streamer's federated posts. Requires `fediverse.inbound`. */
374
488
  onFediverseReply?(post: FediverseInboundPost): void | Promise<void>;
375
489
 
376
490
  /** HTTP request handler. Called for any path under /plugins/<name>/ that
@@ -378,6 +492,15 @@ export interface PluginDef {
378
492
  * on `req.authenticated` yourself. Requires `http.serve` permission. */
379
493
  onHttpRequest?(req: IncomingHttpRequest): OutgoingHttpResponse;
380
494
 
495
+ /** Re-validate a viewer's gate session on page load. Only meaningful for the
496
+ * active `auth.gate` plugin: the host calls it on the viewer's `/` request
497
+ * with the resolved `req.user`, and acts on the verdict: `ok` to continue,
498
+ * `refresh` to extend the session, `deny` to revoke it and force re-login.
499
+ * Optional. Without it a granted session lasts until its cookie
500
+ * expires (no mid-session revocation). This is the revocation hook: return
501
+ * `deny` for users your provider has banned/deleted. Requires `auth.gate`. */
502
+ onAuthCheck?(req: AuthCheckRequest): AuthCheckResult;
503
+
381
504
  /** Render HTML for a dynamic tab. Called by the host when the tab was
382
505
  * declared in the manifest without a static `content` file. Return the
383
506
  * full HTML string to inline as the tab body. `req.user` is the viewer's
@@ -390,9 +513,32 @@ export interface PluginDef {
390
513
  * `req.user` is the viewer's chat identity when available. */
391
514
  onPageContent?(req: ContentRequest): string;
392
515
 
393
- /** Handlers for plugin-emitted custom events. The key is the event type
394
- * string (e.g. "announcement.broadcast"). Notifications only, to filter
395
- * custom events, additional API will be needed. */
516
+ /** Return CSS to inline into the viewer page at request time, the dynamic
517
+ * counterpart to `manifest.styles`, applied to the whole UI. Called once
518
+ * per `/api/config` for any plugin holding `ui.modify`. No manifest field
519
+ * is needed, just export this handler. Return nothing (a bare `return`, or
520
+ * `""`) to contribute nothing. The output is appended after any static
521
+ * `manifest.styles` files, so returning only the active override wins
522
+ * within your plugin's own styles. Plugin styles sit below the admin's
523
+ * appearance settings, so an admin's explicit colors override yours.
524
+ * Global (no per-viewer argument) so `/api/config` stays cacheable.
525
+ * Requires `ui.modify`. */
526
+ onPageStyles?(): string | null | void;
527
+
528
+ /** Return JavaScript to append to the viewer page at request time, the
529
+ * dynamic counterpart to `manifest.scripts`. Called once per `/api/config`
530
+ * for any plugin holding `ui.modify`. The host wraps each plugin's script
531
+ * (static and dynamic) in a try/catch so a runtime error can't break other
532
+ * plugins, but it runs in the shared viewer `window`: wrap your code in an
533
+ * IIFE to avoid global collisions, and escape any untrusted strings you
534
+ * embed. Return nothing (a bare `return`, or `""`) to contribute nothing.
535
+ * Requires `ui.modify`. */
536
+ onPageScripts?(): string | null | void;
537
+
538
+ /** Handlers for custom events owned by this plugin. Keys are local hook names
539
+ * such as "announcement.broadcast". The host registers each hook as
540
+ * `<your-slug>.<hook>`, which emitters use as the target event type.
541
+ * Notifications only. Filtering custom events requires additional API. */
396
542
  on?: { [eventType: string]: (payload: any) => void | Promise<void> };
397
543
 
398
544
  /** Filter chain priority (lower = earlier). Applies to every filter*
@@ -407,65 +553,57 @@ export interface CommandContext {
407
553
  /** The originating chat message. */
408
554
  msg: ChatMessage;
409
555
  /** The sender (same as `msg.user`). */
410
- user?: ChatUser;
556
+ user?: User;
411
557
  /** The canonical command name that matched (not the alias used). */
412
558
  command: string;
559
+ /** The command name or alias exactly as the sender typed it. */
560
+ invokedAs: string;
413
561
  /** Whitespace-split arguments after the command word. */
414
562
  args: string[];
415
563
  /** The raw argument string (everything after the command word, trimmed). */
416
564
  argString: string;
417
565
  /** Post a public reply as the plugin's chat bot. */
418
566
  reply(text: string): void;
419
- /** Whisper a reply to the sender; falls back to a public post if their
567
+ /** Whisper a reply to the sender, falling back to a public post if their
420
568
  * connection is unknown. */
421
569
  replyPrivately(text: string): void;
422
570
  }
423
571
 
424
- /** One command in a {@link defineCommands} table. */
572
+ /** One command in a declarative command table. */
425
573
  export interface CommandDefinition {
426
- /** Short, human-readable summary of what the command does. Surfaced in
427
- * command listings (e.g. a future `!help`); ignored by the router itself. */
574
+ /** Short, human-readable summary shown in command listings. */
428
575
  description?: string;
429
576
  /** Optional usage/example string, e.g. "!latency <0-4>". */
430
577
  usage?: string;
431
578
  /** Alternate names that invoke this command. */
432
579
  aliases?: string[];
433
- /** Only allow senders whose scopes include "MODERATOR". */
580
+ /** Dispatch only for senders whose scopes include "MODERATOR". */
434
581
  modOnly?: boolean;
435
- /** Minimum milliseconds between invocations per user (clocked off
436
- * `msg.timestamp`). */
582
+ /** Non-negative integer milliseconds between invocations per user. */
437
583
  cooldownMs?: number;
438
584
  /** Invoked when the command runs. */
439
585
  run(ctx: CommandContext): void;
440
- /** Invoked instead of `run` when a non-moderator calls a `modOnly` command. */
441
- onDenied?(ctx: CommandContext): void;
442
- /** Invoked instead of `run` when the per-user cooldown hasn't elapsed. */
443
- onCooldown?(ctx: CommandContext): void;
444
586
  }
445
587
 
446
- export interface CommandsConfig {
447
- /** Command prefix. Default `"!"`. */
448
- prefix?: string;
449
- /** Match command names case-sensitively. Default false. */
450
- caseSensitive?: boolean;
451
- commands: Record<string, CommandDefinition>;
452
- /** Fallback when a prefixed message matches no command. */
453
- onUnknown?(ctx: CommandContext): void;
454
- /** Default denied/cooldown handlers, used when a command omits its own. */
455
- onDenied?(ctx: CommandContext): void;
456
- onCooldown?(ctx: CommandContext): void;
457
- }
458
-
459
- /** Build a chat-command router (prefix parsing, aliases, per-user cooldowns,
460
- * moderator gating). Feed the returned function a `ChatMessage`; it returns
461
- * true when the message was a command (even if gated), false otherwise. */
462
- export function defineCommands(
463
- config: CommandsConfig,
464
- ): (msg: ChatMessage) => boolean;
465
-
466
- /** Typed wrappers around the Owncast host. Each method throws if the
467
- * corresponding permission was not declared in plugin.manifest.json. */
588
+ /** Internal payload for a matched command declaration. */
589
+ export interface CommandEvent {
590
+ message: ChatMessage;
591
+ command: string;
592
+ invokedAs: string;
593
+ args: string[];
594
+ argString: string;
595
+ }
596
+
597
+ /** Typed wrappers around the Owncast host. Methods that require a permission
598
+ * say so in their documentation and throw when it was not declared. */
468
599
  export const owncast: {
600
+ /** Write a plugin-attributed entry to Owncast's server log. No permission
601
+ * is required. */
602
+ log: {
603
+ info(message: string): void;
604
+ warning(message: string): void;
605
+ error(message: string): void;
606
+ };
469
607
  chat: {
470
608
  /** Post as the plugin's own chat bot (display name = the plugin's name). */
471
609
  send(text: string): void;
@@ -485,11 +623,13 @@ export const owncast: {
485
623
  * `chat.send`. */
486
624
  replyTo(msg: ChatMessage | number | bigint, text: string): boolean;
487
625
  /** Recent chat history (most recent last). Requires `chat.history`.
488
- * Default limit is 50; pass a smaller number to get fewer. */
626
+ * Default limit is 50. Pass a smaller number to get fewer. */
489
627
  history(limit?: number): ChatMessage[];
490
- /** Hide a chat message by ID. Requires `chat.moderate`. */
628
+ /** Hide a chat message by ID. Throws when the host rejects the operation.
629
+ * Requires `chat.moderate`. */
491
630
  deleteMessage(messageId: string): void;
492
- /** Disconnect a chat client by its numeric ID. Requires `chat.moderate`. */
631
+ /** Disconnect a chat client by its numeric ID. Throws when the host rejects
632
+ * the operation. Requires `chat.moderate`. */
493
633
  kick(clientId: number | bigint): void;
494
634
  /** List currently-connected chat clients. Requires `chat.history`. */
495
635
  clients(): ChatClient[];
@@ -500,17 +640,39 @@ export const owncast: {
500
640
  list(): User[];
501
641
  /** Fetch one user by ID. Requires `users.read`. */
502
642
  get(id: string): User | null;
503
- /** Enable/disable a user; reason is optional. Requires `users.moderate`. */
643
+ /** Enable or disable a user, with an optional reason. Throws when the host
644
+ * rejects the operation. Requires `users.moderate`. */
504
645
  setEnabled(id: string, enabled: boolean, reason?: string): void;
505
- /** Ban an IP address. Requires `users.moderate`. */
646
+ /** Ban an IP address. Throws when the host rejects the operation. Requires
647
+ * `users.moderate`. */
506
648
  banIP(ip: string): void;
649
+ /** Find or create an authenticated user for an external identity. The host
650
+ * scopes `authId` to this plugin's slug. `profileUrl` and `handle`
651
+ * describe a verified external profile, and `public` opts that identity
652
+ * into public display. Returns `{ userId }`. Throws on host error.
653
+ * Requires `users.register`. */
654
+ register(opts: UserRegisterRequest | string): UserRegisterResult;
655
+ };
656
+ /** Viewer-authentication gate. Only a plugin holding `auth.gate` (and enabled
657
+ * by an admin) can issue sessions, and only inside `onHttpRequest`, where the
658
+ * host attaches or clears the signed session cookie on the response. The
659
+ * admin selects the cumulative, host-owned access mode. Plugins cannot read
660
+ * or change it. */
661
+ auth: {
662
+ /** Issue a gate session for an already-registered user (see
663
+ * `users.register`). `ttl` is optional seconds (0/omitted = host default).
664
+ * Throws on host error. Requires `auth.gate`. */
665
+ grantSession(opts: GrantSessionRequest | string): void;
666
+ /** Clear the current viewer's gate session (logout). The plugin still owns
667
+ * the response/redirect. Requires `auth.gate`. */
668
+ endSession(): void;
507
669
  };
508
- /** Upload bytes to Owncast's storage backend (local or S3); returns a
670
+ /** Upload bytes to Owncast's storage backend (local or S3). Returns a
509
671
  * public URL. Requires `storage.upload`. */
510
672
  storage: {
511
673
  upload(name: string, data: Uint8Array | string): UploadResult | null;
512
674
  };
513
- /** Private, sandboxed filesystem under data/plugin-data/<slug>/. The bytes
675
+ /** Private, sandboxed filesystem under data/plugin-storage/<slug>/files/. The bytes
514
676
  * stay server-side (never served over HTTP) and the host confines every
515
677
  * path to this plugin's own directory. All methods require `storage.fs`. */
516
678
  fs: {
@@ -520,15 +682,38 @@ export const owncast: {
520
682
  readText(path: string): string | null;
521
683
  /** Write bytes or a string, creating parent directories as needed. */
522
684
  write(path: string, data: Uint8Array | string): FsResult;
523
- /** List entry names directly inside dir; missing dir lists as empty. */
685
+ /** List entry names directly inside dir. A missing dir lists as empty. */
524
686
  list(dir: string): string[];
525
687
  /** Remove a single file or empty directory. */
526
688
  delete(path: string): FsResult;
527
689
  /** Report whether a path exists inside the sandbox. */
528
690
  exists(path: string): boolean;
529
691
  };
692
+ /** Private SQLite database, one per plugin, stored in `db/` next to the
693
+ * `storage.fs` sandbox in `files/`, outside anything `owncast.fs.*` can name,
694
+ * and quota'd separately. Every call runs with a 2 second timeout. Absence
695
+ * of `error` means success. An error, missing response, or non-object
696
+ * response throws. JavaScript loses unsafe integers before `JSON.stringify`
697
+ * on writes and during `JSON.parse` on reads, so store values above
698
+ * `Number.MAX_SAFE_INTEGER` (2^53 - 1) as TEXT when they must remain exact.
699
+ * Requires `storage.sql`. */
700
+ sql: {
701
+ /** Execute one statement batch as a single transaction: it commits whole
702
+ * or leaves the database untouched. A transaction cannot stay open
703
+ * across calls. */
704
+ exec(sql: string, params?: SQLValue[]): SQLExecResult;
705
+ /** Query rows as objects keyed by column name. Alias duplicate columns.
706
+ * The result is never silently shortened: a query returning more than
707
+ * 10000 rows, or more than 1 MiB of encoded data, throws asking for a
708
+ * LIMIT. */
709
+ query(sql: string, params?: SQLValue[]): SQLRow[];
710
+ /** Return the first matching row, or null. Only that row is read back, so
711
+ * this stays under the result budget on a table `query` is too big
712
+ * for. */
713
+ queryRow(sql: string, params?: SQLValue[]): SQLRow | null;
714
+ };
530
715
  /** Post to the fediverse on the streamer's behalf. Requires `fediverse.post`,
531
- * which is high-trust (posts go out under the streamer's own handle);
716
+ * which is high-trust (posts go out under the streamer's own handle), so
532
717
  * admins should grant it sparingly. */
533
718
  fediverse: {
534
719
  /** Publish a public, text-only post. Returns `{ url }` (currently empty
@@ -549,6 +734,7 @@ export const owncast: {
549
734
  };
550
735
  kv: {
551
736
  get(key: string): string | null;
737
+ /** Store a value. Throws when the host rejects the operation. */
552
738
  set(key: string, value: string | number): void;
553
739
  /** Read a JSON value, parsed. Returns `fallback` (default `undefined`)
554
740
  * when the key is unset or holds invalid JSON. Requires `storage.kv`. */
@@ -557,7 +743,7 @@ export const owncast: {
557
743
  setJSON(key: string, value: unknown): void;
558
744
  };
559
745
  /** Read this plugin's admin-configurable settings, declared under
560
- * `config` in the manifest. Ambient no permission required. */
746
+ * `config` in the manifest. Ambient, so no permission is required. */
561
747
  config: {
562
748
  /** The effective value of a manifest-declared config key (admin override,
563
749
  * else the declared default), parsed to its declared type. Returns
@@ -565,10 +751,10 @@ export const owncast: {
565
751
  * value. */
566
752
  get<T = unknown>(key: string, fallback?: T): T;
567
753
  };
568
- /** Read files the plugin bundled in its own `assets/` directory templates,
754
+ /** Read files the plugin bundled in its own `assets/` directory: templates,
569
755
  * data files, and other bundled resources loaded at request time. Path is
570
- * relative to `assets/` and must not contain `..`. Ambient no permission
571
- * required. */
756
+ * relative to `assets/` and must not contain `..`. Ambient, so no permission
757
+ * is required. */
572
758
  assets: {
573
759
  /** Raw bytes of the file, or `null` if not found. */
574
760
  read(path: string): Uint8Array | null;
@@ -576,6 +762,8 @@ export const owncast: {
576
762
  readText(path: string): string | null;
577
763
  };
578
764
  events: {
765
+ /** Emit to a custom hook using its fully qualified
766
+ * `<recipient-plugin-slug>.<hook>` name. Requires `events.emit`. */
579
767
  emit(eventType: string, payload: unknown): void;
580
768
  };
581
769
  /** Control over the viewer action buttons this plugin contributes.
@@ -585,30 +773,30 @@ export const owncast: {
585
773
  actions: {
586
774
  /** Append one or more buttons to the plugin's runtime list. Each
587
775
  * entry is validated with the same rules as `manifest.actions`
588
- * (title required; exactly one of `url` or `html`; relative URLs
589
- * rewritten into this plugin's namespace; cross-plugin URLs
590
- * rejected). The next viewer `/api/config` request returns
591
- * `manifest.actions` ++ the runtime list. */
776
+ * (title required, exactly one of `url` or `html`, relative URLs
777
+ * rewritten into this plugin's namespace, cross-plugin URLs
778
+ * rejected). Throws when the host rejects the action list. */
592
779
  add(actions: ActionButton | ActionButton[]): void;
593
- /** Drop the runtime additions; only `manifest.actions` remain on
594
- * the next viewer `/api/config` request. */
780
+ /** Drop the runtime additions, so only `manifest.actions` remain on
781
+ * the next viewer `/api/config` request. Throws when the host rejects the
782
+ * operation. */
595
783
  clear(): void;
596
784
  };
597
785
  sse: {
598
786
  /** Push one Server-Sent-Event to every browser connected to this
599
787
  * plugin's `/plugins/<name>/_sse/<channel>` stream. `event` is the SSE
600
- * event name (`""` → the default "message" event); `data` is sent as-is
601
- * if a string, otherwise JSON-stringified. Fire-and-forget; frames to a
788
+ * event name (`""` → the default "message" event). `data` is sent as-is
789
+ * if a string, otherwise JSON-stringified. Fire-and-forget, and frames to a
602
790
  * slow client are dropped rather than blocking the plugin. Requires the
603
791
  * `http.sse` permission. */
604
792
  send(channel: string, event: string, data: unknown): void;
605
793
  };
606
- /** Host-driven timers. The sandbox has no setTimeout; these ask the host to
794
+ /** Host-driven timers. The sandbox has no setTimeout, so these ask the host to
607
795
  * call your callback back later (in this instance). No permission required.
608
796
  * Timers do not survive a plugin reload or host restart. */
609
797
  timer: {
610
798
  /** Run `fn` once after ~`ms` milliseconds. Returns an id for `clear()`.
611
- * Very small delays are clamped up by the host; throws past the
799
+ * Very small delays are clamped up by the host. Throws past the
612
800
  * per-plugin pending-timer cap. */
613
801
  setTimeout(fn: () => void, ms: number): number;
614
802
  /** Run `fn` every ~`ms` milliseconds until `clear()`. The next run is
@@ -635,7 +823,8 @@ export const owncast: {
635
823
  tags(): string[];
636
824
  };
637
825
  /** Read/change video/transcoding configuration. read() requires
638
- * `videoconfig.read`; write() requires `videoconfig.write`. */
826
+ * `videoconfig.read`. write() requires `videoconfig.write` and throws when
827
+ * the host rejects the update or does not return an operation result. */
639
828
  videoConfig: {
640
829
  read(): VideoConfig;
641
830
  write(config: VideoConfigUpdate): void;
@@ -656,7 +845,7 @@ export interface HttpResponse {
656
845
 
657
846
  /** An entry in `manifest.actions`, declares an action button the Owncast
658
847
  * UI surfaces while this plugin is enabled. Mirrors Owncast's existing
659
- * ExternalAction shape; the host merges enabled-plugin buttons with the
848
+ * ExternalAction shape. The host merges enabled-plugin buttons with the
660
849
  * admin-configured list.
661
850
  *
662
851
  * Exactly one of `url` or `html` is required.
@@ -687,11 +876,46 @@ export interface ActionButton {
687
876
 
688
877
  /** `manifest.network`, narrows outbound HTTP scope for plugins that
689
878
  * declare the `network.fetch` permission. Required when that permission
690
- * is granted; the host rejects loads otherwise. */
879
+ * is granted. The host rejects loads otherwise. */
691
880
  export interface NetworkConfig {
692
881
  /** Hostname globs the plugin can reach via `owncast.http.fetch`.
693
- * Bare names match exactly (`"api.discord.com"`); `*` is a wildcard
882
+ * Bare names match exactly (`"api.discord.com"`), and `*` is a wildcard
694
883
  * segment (`"*.weather.com"`). The bare wildcard `"*"` matches any
695
884
  * host but must be written explicitly. */
696
885
  allowedHosts: string[];
697
886
  }
887
+
888
+ /** `manifest.category` (optional), the plugin's registry browse category.
889
+ * One of the canonical slugs below. The plugin registry uses it to filter
890
+ * the browse listing; unknown values are tolerated but won't match any
891
+ * filter.
892
+ *
893
+ * - `chat-bots`: Chat bots
894
+ * - `chat-filters`: Chat filters
895
+ * - `moderation`: Moderation
896
+ * - `authentication`: Authentication
897
+ * - `themes`: Themes
898
+ * - `overlays`: Overlays & widgets
899
+ * - `notifications`: Notifications
900
+ * - `integrations`: Integrations
901
+ * - `video`: Video & streaming
902
+ * - `analytics`: Analytics & stats
903
+ * - `games`: Games & fun
904
+ * - `admin-utilities`: Admin utilities
905
+ * - `examples`: Examples
906
+ * - `other`: Other */
907
+ export type PluginCategory =
908
+ | "chat-bots"
909
+ | "chat-filters"
910
+ | "moderation"
911
+ | "authentication"
912
+ | "themes"
913
+ | "overlays"
914
+ | "notifications"
915
+ | "integrations"
916
+ | "video"
917
+ | "analytics"
918
+ | "games"
919
+ | "admin-utilities"
920
+ | "examples"
921
+ | "other";